JellyBeanAccessibilityInjector.java revision f2477e01787aa58f445919b809d89e252beef54f
1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package org.chromium.content.browser.accessibility;
6
7import android.content.Context;
8import android.os.Bundle;
9import android.os.SystemClock;
10import android.view.accessibility.AccessibilityNodeInfo;
11
12import org.chromium.content.browser.ContentViewCore;
13import org.chromium.content.browser.JavascriptInterface;
14import org.json.JSONException;
15import org.json.JSONObject;
16
17import java.util.Iterator;
18import java.util.concurrent.atomic.AtomicInteger;
19
20/**
21 * Handles injecting accessibility Javascript and related Javascript -> Java APIs for JB and newer
22 * devices.
23 */
24class JellyBeanAccessibilityInjector extends AccessibilityInjector {
25    private CallbackHandler mCallback;
26    private JSONObject mAccessibilityJSONObject;
27
28    private static final String ALIAS_TRAVERSAL_JS_INTERFACE = "accessibilityTraversal";
29
30    // Template for JavaScript that performs AndroidVox actions.
31    private static final String ACCESSIBILITY_ANDROIDVOX_TEMPLATE =
32            "cvox.AndroidVox.performAction('%1s')";
33
34    /**
35     * Constructs an instance of the JellyBeanAccessibilityInjector.
36     * @param view The ContentViewCore that this AccessibilityInjector manages.
37     */
38    protected JellyBeanAccessibilityInjector(ContentViewCore view) {
39        super(view);
40    }
41
42    @Override
43    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
44        info.setMovementGranularities(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER |
45                AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD |
46                AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE |
47                AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH |
48                AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PAGE);
49        info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
50        info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
51        info.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT);
52        info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT);
53        info.addAction(AccessibilityNodeInfo.ACTION_CLICK);
54        info.setClickable(true);
55    }
56
57    @Override
58    public boolean supportsAccessibilityAction(int action) {
59        if (action == AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY ||
60                action == AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY ||
61                action == AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT ||
62                action == AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT ||
63                action == AccessibilityNodeInfo.ACTION_CLICK) {
64            return true;
65        }
66
67        return false;
68    }
69
70    @Override
71    public boolean performAccessibilityAction(int action, Bundle arguments) {
72        if (!accessibilityIsAvailable() || !mContentViewCore.isAlive() ||
73                !mInjectedScriptEnabled || !mScriptInjected) {
74            return false;
75        }
76
77        boolean actionSuccessful = sendActionToAndroidVox(action, arguments);
78
79        if (actionSuccessful) mContentViewCore.showImeIfNeeded();
80
81        return actionSuccessful;
82    }
83
84    @Override
85    protected void addAccessibilityApis() {
86        super.addAccessibilityApis();
87
88        Context context = mContentViewCore.getContext();
89        if (context != null && mCallback == null) {
90            mCallback = new CallbackHandler(ALIAS_TRAVERSAL_JS_INTERFACE);
91            mContentViewCore.addJavascriptInterface(mCallback, ALIAS_TRAVERSAL_JS_INTERFACE);
92        }
93    }
94
95    @Override
96    protected void removeAccessibilityApis() {
97        super.removeAccessibilityApis();
98
99        if (mCallback != null) {
100            mContentViewCore.removeJavascriptInterface(ALIAS_TRAVERSAL_JS_INTERFACE);
101            mCallback = null;
102        }
103    }
104
105    /**
106     * Packs an accessibility action into a JSON object and sends it to AndroidVox.
107     *
108     * @param action The action identifier.
109     * @param arguments The action arguments, if applicable.
110     * @return The result of the action.
111     */
112    private boolean sendActionToAndroidVox(int action, Bundle arguments) {
113        if (mCallback == null) return false;
114        if (mAccessibilityJSONObject == null) {
115            mAccessibilityJSONObject = new JSONObject();
116        } else {
117            // Remove all keys from the object.
118            final Iterator<?> keys = mAccessibilityJSONObject.keys();
119            while (keys.hasNext()) {
120                keys.next();
121                keys.remove();
122            }
123        }
124
125        try {
126            mAccessibilityJSONObject.accumulate("action", action);
127            if (arguments != null) {
128                if (action == AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY ||
129                        action == AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY) {
130                    final int granularity = arguments.getInt(AccessibilityNodeInfo.
131                            ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
132                    mAccessibilityJSONObject.accumulate("granularity", granularity);
133                } else if (action == AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT ||
134                        action == AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT) {
135                    final String element = arguments.getString(
136                            AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING);
137                    mAccessibilityJSONObject.accumulate("element", element);
138                }
139            }
140        } catch (JSONException ex) {
141            return false;
142        }
143
144        final String jsonString = mAccessibilityJSONObject.toString();
145        final String jsCode = String.format(ACCESSIBILITY_ANDROIDVOX_TEMPLATE, jsonString);
146        return mCallback.performAction(mContentViewCore, jsCode);
147    }
148
149    private static class CallbackHandler {
150        private static final String JAVASCRIPT_ACTION_TEMPLATE =
151                "(function() {" +
152                "  retVal = false;" +
153                "  try {" +
154                "    retVal = %s;" +
155                "  } catch (e) {" +
156                "    retVal = false;" +
157                "  }" +
158                "  %s.onResult(%d, retVal);" +
159                "})()";
160
161        // Time in milliseconds to wait for a result before failing.
162        private static final long RESULT_TIMEOUT = 5000;
163
164        private final AtomicInteger mResultIdCounter = new AtomicInteger();
165        private final Object mResultLock = new Object();
166        private final String mInterfaceName;
167
168        private boolean mResult = false;
169        private long mResultId = -1;
170
171        private CallbackHandler(String interfaceName) {
172            mInterfaceName = interfaceName;
173        }
174
175        /**
176         * Performs an action and attempts to wait for a result.
177         *
178         * @param contentView The ContentViewCore to perform the action on.
179         * @param code Javascript code that evaluates to a result.
180         * @return The result of the action.
181         */
182        private boolean performAction(ContentViewCore contentView, String code) {
183            final int resultId = mResultIdCounter.getAndIncrement();
184            final String js = String.format(JAVASCRIPT_ACTION_TEMPLATE, code, mInterfaceName,
185                    resultId);
186            contentView.evaluateJavaScript(js, null);
187
188            return getResultAndClear(resultId);
189        }
190
191        /**
192         * Gets the result of a request to perform an accessibility action.
193         *
194         * @param resultId The result id to match the result with the request.
195         * @return The result of the request.
196         */
197        private boolean getResultAndClear(int resultId) {
198            synchronized (mResultLock) {
199                final boolean success = waitForResultTimedLocked(resultId);
200                final boolean result = success ? mResult : false;
201                clearResultLocked();
202                return result;
203            }
204        }
205
206        /**
207         * Clears the result state.
208         */
209        private void clearResultLocked() {
210            mResultId = -1;
211            mResult = false;
212        }
213
214        /**
215         * Waits up to a given bound for a result of a request and returns it.
216         *
217         * @param resultId The result id to match the result with the request.
218         * @return Whether the result was received.
219         */
220        private boolean waitForResultTimedLocked(int resultId) {
221            long waitTimeMillis = RESULT_TIMEOUT;
222            final long startTimeMillis = SystemClock.uptimeMillis();
223            while (true) {
224                try {
225                    if (mResultId == resultId) return true;
226                    if (mResultId > resultId) return false;
227                    final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
228                    waitTimeMillis = RESULT_TIMEOUT - elapsedTimeMillis;
229                    if (waitTimeMillis <= 0) return false;
230                    mResultLock.wait(waitTimeMillis);
231                } catch (InterruptedException ie) {
232                    /* ignore */
233                }
234            }
235        }
236
237        /**
238         * Callback exposed to JavaScript.  Handles returning the result of a
239         * request to a waiting (or potentially timed out) thread.
240         *
241         * @param id The result id of the request as a {@link String}.
242         * @param result The result of a request as a {@link String}.
243         */
244        @JavascriptInterface
245        @SuppressWarnings("unused")
246        public void onResult(String id, String result) {
247            final long resultId;
248            try {
249                resultId = Long.parseLong(id);
250            } catch (NumberFormatException e) {
251                return;
252            }
253
254            synchronized (mResultLock) {
255                if (resultId > mResultId) {
256                    mResult = Boolean.parseBoolean(result);
257                    mResultId = resultId;
258                }
259                mResultLock.notifyAll();
260            }
261        }
262    }
263}
264