JellyBeanAccessibilityInjector.java revision c2e0dbddbe15c98d52c4786dac06cb8952a8ae6d
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.View; 11import android.view.accessibility.AccessibilityNodeInfo; 12 13import org.chromium.content.browser.ContentViewCore; 14import org.chromium.content.browser.JavascriptInterface; 15import org.json.JSONException; 16import org.json.JSONObject; 17 18import java.util.Iterator; 19import java.util.concurrent.atomic.AtomicInteger; 20 21/** 22 * Handles injecting accessibility Javascript and related Javascript -> Java APIs for JB and newer 23 * devices. 24 */ 25class 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(ACCESSIBILITY_ANDROIDVOX_TEMPLATE, jsonString); 147 return mCallback.performAction(mContentViewCore, jsCode); 148 } 149 150 private static class CallbackHandler { 151 private static final String JAVASCRIPT_ACTION_TEMPLATE = 152 "(function() {" + 153 " retVal = false;" + 154 " try {" + 155 " retVal = %s;" + 156 " } catch (e) {" + 157 " retVal = false;" + 158 " }" + 159 " %s.onResult(%d, retVal);" + 160 "})()"; 161 162 // Time in milliseconds to wait for a result before failing. 163 private static final long RESULT_TIMEOUT = 5000; 164 165 private final AtomicInteger mResultIdCounter = new AtomicInteger(); 166 private final Object mResultLock = new Object(); 167 private final String mInterfaceName; 168 169 private boolean mResult = false; 170 private long mResultId = -1; 171 172 private CallbackHandler(String interfaceName) { 173 mInterfaceName = interfaceName; 174 } 175 176 /** 177 * Performs an action and attempts to wait for a result. 178 * 179 * @param contentView The ContentViewCore to perform the action on. 180 * @param code Javascript code that evaluates to a result. 181 * @return The result of the action. 182 */ 183 private boolean performAction(ContentViewCore contentView, String code) { 184 final int resultId = mResultIdCounter.getAndIncrement(); 185 final String js = String.format(JAVASCRIPT_ACTION_TEMPLATE, code, mInterfaceName, 186 resultId); 187 contentView.evaluateJavaScript(js, null); 188 189 return getResultAndClear(resultId); 190 } 191 192 /** 193 * Gets the result of a request to perform an accessibility action. 194 * 195 * @param resultId The result id to match the result with the request. 196 * @return The result of the request. 197 */ 198 private boolean getResultAndClear(int resultId) { 199 synchronized (mResultLock) { 200 final boolean success = waitForResultTimedLocked(resultId); 201 final boolean result = success ? mResult : false; 202 clearResultLocked(); 203 return result; 204 } 205 } 206 207 /** 208 * Clears the result state. 209 */ 210 private void clearResultLocked() { 211 mResultId = -1; 212 mResult = false; 213 } 214 215 /** 216 * Waits up to a given bound for a result of a request and returns it. 217 * 218 * @param resultId The result id to match the result with the request. 219 * @return Whether the result was received. 220 */ 221 private boolean waitForResultTimedLocked(int resultId) { 222 long waitTimeMillis = RESULT_TIMEOUT; 223 final long startTimeMillis = SystemClock.uptimeMillis(); 224 while (true) { 225 try { 226 if (mResultId == resultId) return true; 227 if (mResultId > resultId) return false; 228 final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; 229 waitTimeMillis = RESULT_TIMEOUT - elapsedTimeMillis; 230 if (waitTimeMillis <= 0) return false; 231 mResultLock.wait(waitTimeMillis); 232 } catch (InterruptedException ie) { 233 /* ignore */ 234 } 235 } 236 } 237 238 /** 239 * Callback exposed to JavaScript. Handles returning the result of a 240 * request to a waiting (or potentially timed out) thread. 241 * 242 * @param id The result id of the request as a {@link String}. 243 * @param result The result of a request as a {@link String}. 244 */ 245 @JavascriptInterface 246 @SuppressWarnings("unused") 247 public void onResult(String id, String result) { 248 final long resultId; 249 try { 250 resultId = Long.parseLong(id); 251 } catch (NumberFormatException e) { 252 return; 253 } 254 255 synchronized (mResultLock) { 256 if (resultId > mResultId) { 257 mResult = Boolean.parseBoolean(result); 258 mResultId = resultId; 259 } 260 mResultLock.notifyAll(); 261 } 262 } 263 } 264} 265