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