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. 4 5 package org.chromium.content.browser.accessibility; 6 7 import android.content.Context; 8 import android.os.Bundle; 9 import android.os.SystemClock; 10 import android.view.accessibility.AccessibilityNodeInfo; 11 12 import org.chromium.content.browser.ContentViewCore; 13 import org.chromium.content.browser.JavascriptInterface; 14 import org.json.JSONException; 15 import org.json.JSONObject; 16 17 import java.util.Iterator; 18 import java.util.Locale; 19 import java.util.concurrent.atomic.AtomicInteger; 20 21 /** 22 * Handles injecting accessibility Javascript and related Javascript -> Java APIs for JB and newer 23 * devices. 24 */ 25 class JellyBeanAccessibilityInjector extends AccessibilityInjector { 26 private CallbackHandler mCallback; 27 private JSONObject mAccessibilityJSONObject; 28 29 private static final String ALIAS_TRAVERSAL_JS_INTERFACE = "accessibilityTraversal"; 30 31 // Template for JavaScript that performs AndroidVox actions. 32 private static final String ACCESSIBILITY_ANDROIDVOX_TEMPLATE = 33 "cvox.AndroidVox.performAction('%1s')"; 34 35 /** 36 * Constructs an instance of the JellyBeanAccessibilityInjector. 37 * @param view The ContentViewCore that this AccessibilityInjector manages. 38 */ 39 protected JellyBeanAccessibilityInjector(ContentViewCore view) { 40 super(view); 41 } 42 43 @Override 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); 56 } 57 58 @Override 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) { 65 return true; 66 } 67 68 return false; 69 } 70 71 @Override 72 public boolean performAccessibilityAction(int action, Bundle arguments) { 73 if (!accessibilityIsAvailable() || !mContentViewCore.isAlive() || 74 !mInjectedScriptEnabled || !mScriptInjected) { 75 return false; 76 } 77 78 boolean actionSuccessful = sendActionToAndroidVox(action, arguments); 79 80 if (actionSuccessful) mContentViewCore.showImeIfNeeded(); 81 82 return actionSuccessful; 83 } 84 85 @Override 86 protected void addAccessibilityApis() { 87 super.addAccessibilityApis(); 88 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); 93 } 94 } 95 96 @Override 97 protected void removeAccessibilityApis() { 98 super.removeAccessibilityApis(); 99 100 if (mCallback != null) { 101 mContentViewCore.removeJavascriptInterface(ALIAS_TRAVERSAL_JS_INTERFACE); 102 mCallback = null; 103 } 104 } 105 106 /** 107 * Packs an accessibility action into a JSON object and sends it to AndroidVox. 108 * 109 * @param action The action identifier. 110 * @param arguments The action arguments, if applicable. 111 * @return The result of the action. 112 */ 113 private boolean sendActionToAndroidVox(int action, Bundle arguments) { 114 if (mCallback == null) return false; 115 if (mAccessibilityJSONObject == null) { 116 mAccessibilityJSONObject = new JSONObject(); 117 } else { 118 // Remove all keys from the object. 119 final Iterator<?> keys = mAccessibilityJSONObject.keys(); 120 while (keys.hasNext()) { 121 keys.next(); 122 keys.remove(); 123 } 124 } 125 126 try { 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(AccessibilityNodeInfo. 132 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); 139 } 140 } 141 } catch (JSONException ex) { 142 return false; 143 } 144 145 final String jsonString = mAccessibilityJSONObject.toString(); 146 final String jsCode = String.format(Locale.US, ACCESSIBILITY_ANDROIDVOX_TEMPLATE, 147 jsonString); 148 return mCallback.performAction(mContentViewCore, jsCode); 149 } 150 151 private static class CallbackHandler { 152 private static final String JAVASCRIPT_ACTION_TEMPLATE = 153 "(function() {" + 154 " retVal = false;" + 155 " try {" + 156 " retVal = %s;" + 157 " } catch (e) {" + 158 " retVal = false;" + 159 " }" + 160 " %s.onResult(%d, retVal);" + 161 "})()"; 162 163 // Time in milliseconds to wait for a result before failing. 164 private static final long RESULT_TIMEOUT = 5000; 165 166 private final AtomicInteger mResultIdCounter = new AtomicInteger(); 167 private final Object mResultLock = new Object(); 168 private final String mInterfaceName; 169 170 private boolean mResult = false; 171 private long mResultId = -1; 172 173 private CallbackHandler(String interfaceName) { 174 mInterfaceName = interfaceName; 175 } 176 177 /** 178 * Performs an action and attempts to wait for a result. 179 * 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. 183 */ 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.evaluateJavaScript(js, null); 189 190 return getResultAndClear(resultId); 191 } 192 193 /** 194 * Gets the result of a request to perform an accessibility action. 195 * 196 * @param resultId The result id to match the result with the request. 197 * @return The result of the request. 198 */ 199 private boolean getResultAndClear(int resultId) { 200 synchronized (mResultLock) { 201 final boolean success = waitForResultTimedLocked(resultId); 202 final boolean result = success ? mResult : false; 203 clearResultLocked(); 204 return result; 205 } 206 } 207 208 /** 209 * Clears the result state. 210 */ 211 private void clearResultLocked() { 212 mResultId = -1; 213 mResult = false; 214 } 215 216 /** 217 * Waits up to a given bound for a result of a request and returns it. 218 * 219 * @param resultId The result id to match the result with the request. 220 * @return Whether the result was received. 221 */ 222 private boolean waitForResultTimedLocked(int resultId) { 223 long waitTimeMillis = RESULT_TIMEOUT; 224 final long startTimeMillis = SystemClock.uptimeMillis(); 225 while (true) { 226 try { 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) { 234 /* ignore */ 235 } 236 } 237 } 238 239 /** 240 * Callback exposed to JavaScript. Handles returning the result of a 241 * request to a waiting (or potentially timed out) thread. 242 * 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}. 245 */ 246 @JavascriptInterface 247 @SuppressWarnings("unused") 248 public void onResult(String id, String result) { 249 final long resultId; 250 try { 251 resultId = Long.parseLong(id); 252 } catch (NumberFormatException e) { 253 return; 254 } 255 256 synchronized (mResultLock) { 257 if (resultId > mResultId) { 258 mResult = Boolean.parseBoolean(result); 259 mResultId = resultId; 260 } 261 mResultLock.notifyAll(); 262 } 263 } 264 } 265 } 266