1 // Copyright 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.
5 package org.chromium.content.browser.accessibility;
7 import android.content.Context;
8 import android.os.Bundle;
9 import android.os.SystemClock;
10 import android.view.accessibility.AccessibilityNodeInfo;
12 import org.chromium.content.browser.ContentViewCore;
13 import org.chromium.content.browser.JavascriptInterface;
14 import org.json.JSONException;
15 import org.json.JSONObject;
17 import java.util.Iterator;
18 import java.util.Locale;
19 import java.util.concurrent.atomic.AtomicInteger;
22 * Handles injecting accessibility Javascript and related Javascript -> Java APIs for JB and newer
25 class JellyBeanAccessibilityInjector extends AccessibilityInjector {
26 private CallbackHandler mCallback;
27 private JSONObject mAccessibilityJSONObject;
29 private static final String ALIAS_TRAVERSAL_JS_INTERFACE = "accessibilityTraversal";
31 // Template for JavaScript that performs AndroidVox actions.
32 private static final String ACCESSIBILITY_ANDROIDVOX_TEMPLATE =
33 "cvox.AndroidVox.performAction('%1s')";
36 * Constructs an instance of the JellyBeanAccessibilityInjector.
37 * @param view The ContentViewCore that this AccessibilityInjector manages.
39 protected JellyBeanAccessibilityInjector(ContentViewCore view) {
44 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
45 info.setMovementGranularities(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER |
46 AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD |
47 AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE |
48 AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH |
49 AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PAGE);
50 info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
51 info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
52 info.addAction(AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT);
53 info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT);
54 info.addAction(AccessibilityNodeInfo.ACTION_CLICK);
55 info.setClickable(true);
59 public boolean supportsAccessibilityAction(int action) {
60 if (action == AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY ||
61 action == AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY ||
62 action == AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT ||
63 action == AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT ||
64 action == AccessibilityNodeInfo.ACTION_CLICK) {
72 public boolean performAccessibilityAction(int action, Bundle arguments) {
73 if (!accessibilityIsAvailable() || !mContentViewCore.isAlive() ||
74 !mInjectedScriptEnabled || !mScriptInjected) {
78 boolean actionSuccessful = sendActionToAndroidVox(action, arguments);
80 if (actionSuccessful) mContentViewCore.getWebContents().showImeIfNeeded();
82 return actionSuccessful;
86 protected void addAccessibilityApis() {
87 super.addAccessibilityApis();
89 Context context = mContentViewCore.getContext();
90 if (context != null && mCallback == null) {
91 mCallback = new CallbackHandler(ALIAS_TRAVERSAL_JS_INTERFACE);
92 mContentViewCore.addJavascriptInterface(mCallback, ALIAS_TRAVERSAL_JS_INTERFACE);
97 protected void removeAccessibilityApis() {
98 super.removeAccessibilityApis();
100 if (mCallback != null) {
101 mContentViewCore.removeJavascriptInterface(ALIAS_TRAVERSAL_JS_INTERFACE);
107 * Packs an accessibility action into a JSON object and sends it to AndroidVox.
109 * @param action The action identifier.
110 * @param arguments The action arguments, if applicable.
111 * @return The result of the action.
113 private boolean sendActionToAndroidVox(int action, Bundle arguments) {
114 if (mCallback == null) return false;
115 if (mAccessibilityJSONObject == null) {
116 mAccessibilityJSONObject = new JSONObject();
118 // Remove all keys from the object.
119 final Iterator<?> keys = mAccessibilityJSONObject.keys();
120 while (keys.hasNext()) {
127 mAccessibilityJSONObject.accumulate("action", action);
128 if (arguments != null) {
129 if (action == AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY ||
130 action == AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY) {
131 final int granularity = arguments.getInt(
132 AccessibilityNodeInfo.ACTION_ARGUMENT_MOVEMENT_GRANULARITY_INT);
133 mAccessibilityJSONObject.accumulate("granularity", granularity);
134 } else if (action == AccessibilityNodeInfo.ACTION_NEXT_HTML_ELEMENT ||
135 action == AccessibilityNodeInfo.ACTION_PREVIOUS_HTML_ELEMENT) {
136 final String element = arguments.getString(
137 AccessibilityNodeInfo.ACTION_ARGUMENT_HTML_ELEMENT_STRING);
138 mAccessibilityJSONObject.accumulate("element", element);
141 } catch (JSONException ex) {
145 final String jsonString = mAccessibilityJSONObject.toString();
146 final String jsCode = String.format(Locale.US, ACCESSIBILITY_ANDROIDVOX_TEMPLATE,
148 return mCallback.performAction(mContentViewCore, jsCode);
151 private static class CallbackHandler {
152 private static final String JAVASCRIPT_ACTION_TEMPLATE =
160 " %s.onResult(%d, retVal);" +
163 // Time in milliseconds to wait for a result before failing.
164 private static final long RESULT_TIMEOUT = 5000;
166 private final AtomicInteger mResultIdCounter = new AtomicInteger();
167 private final Object mResultLock = new Object();
168 private final String mInterfaceName;
170 private boolean mResult = false;
171 private long mResultId = -1;
173 private CallbackHandler(String interfaceName) {
174 mInterfaceName = interfaceName;
178 * Performs an action and attempts to wait for a result.
180 * @param contentView The ContentViewCore to perform the action on.
181 * @param code Javascript code that evaluates to a result.
182 * @return The result of the action.
184 private boolean performAction(ContentViewCore contentView, String code) {
185 final int resultId = mResultIdCounter.getAndIncrement();
186 final String js = String.format(Locale.US, JAVASCRIPT_ACTION_TEMPLATE, code,
187 mInterfaceName, resultId);
188 contentView.getWebContents().evaluateJavaScript(js, null);
190 return getResultAndClear(resultId);
194 * Gets the result of a request to perform an accessibility action.
196 * @param resultId The result id to match the result with the request.
197 * @return The result of the request.
199 private boolean getResultAndClear(int resultId) {
200 synchronized (mResultLock) {
201 final boolean success = waitForResultTimedLocked(resultId);
202 final boolean result = success ? mResult : false;
209 * Clears the result state.
211 private void clearResultLocked() {
217 * Waits up to a given bound for a result of a request and returns it.
219 * @param resultId The result id to match the result with the request.
220 * @return Whether the result was received.
222 private boolean waitForResultTimedLocked(int resultId) {
223 long waitTimeMillis = RESULT_TIMEOUT;
224 final long startTimeMillis = SystemClock.uptimeMillis();
227 if (mResultId == resultId) return true;
228 if (mResultId > resultId) return false;
229 final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
230 waitTimeMillis = RESULT_TIMEOUT - elapsedTimeMillis;
231 if (waitTimeMillis <= 0) return false;
232 mResultLock.wait(waitTimeMillis);
233 } catch (InterruptedException ie) {
240 * Callback exposed to JavaScript. Handles returning the result of a
241 * request to a waiting (or potentially timed out) thread.
243 * @param id The result id of the request as a {@link String}.
244 * @param result The result of a request as a {@link String}.
247 @SuppressWarnings("unused")
248 public void onResult(String id, String result) {
251 resultId = Long.parseLong(id);
252 } catch (NumberFormatException e) {
256 synchronized (mResultLock) {
257 if (resultId > mResultId) {
258 mResult = Boolean.parseBoolean(result);
259 mResultId = resultId;
261 mResultLock.notifyAll();