JellyBeanAccessibilityInjector.java revision 2a99a7e74a7f215066514fe81d2bfa6639d9eddd
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() { %s.onResult(%d, %s); })()"; 153 154 // Time in milliseconds to wait for a result before failing. 155 private static final long RESULT_TIMEOUT = 5000; 156 157 private final AtomicInteger mResultIdCounter = new AtomicInteger(); 158 private final Object mResultLock = new Object(); 159 private final String mInterfaceName; 160 161 private boolean mResult = false; 162 private long mResultId = -1; 163 164 private CallbackHandler(String interfaceName) { 165 mInterfaceName = interfaceName; 166 } 167 168 /** 169 * Performs an action and attempts to wait for a result. 170 * 171 * @param contentView The ContentViewCore to perform the action on. 172 * @param code Javascript code that evaluates to a result. 173 * @return The result of the action. 174 */ 175 private boolean performAction(ContentViewCore contentView, String code) { 176 final int resultId = mResultIdCounter.getAndIncrement(); 177 final String js = String.format(JAVASCRIPT_ACTION_TEMPLATE, mInterfaceName, resultId, 178 code); 179 contentView.evaluateJavaScript(js, null); 180 181 return getResultAndClear(resultId); 182 } 183 184 /** 185 * Gets the result of a request to perform an accessibility action. 186 * 187 * @param resultId The result id to match the result with the request. 188 * @return The result of the request. 189 */ 190 private boolean getResultAndClear(int resultId) { 191 synchronized (mResultLock) { 192 final boolean success = waitForResultTimedLocked(resultId); 193 final boolean result = success ? mResult : false; 194 clearResultLocked(); 195 return result; 196 } 197 } 198 199 /** 200 * Clears the result state. 201 */ 202 private void clearResultLocked() { 203 mResultId = -1; 204 mResult = false; 205 } 206 207 /** 208 * Waits up to a given bound for a result of a request and returns it. 209 * 210 * @param resultId The result id to match the result with the request. 211 * @return Whether the result was received. 212 */ 213 private boolean waitForResultTimedLocked(int resultId) { 214 long waitTimeMillis = RESULT_TIMEOUT; 215 final long startTimeMillis = SystemClock.uptimeMillis(); 216 while (true) { 217 try { 218 if (mResultId == resultId) return true; 219 if (mResultId > resultId) return false; 220 final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis; 221 waitTimeMillis = RESULT_TIMEOUT - elapsedTimeMillis; 222 if (waitTimeMillis <= 0) return false; 223 mResultLock.wait(waitTimeMillis); 224 } catch (InterruptedException ie) { 225 /* ignore */ 226 } 227 } 228 } 229 230 /** 231 * Callback exposed to JavaScript. Handles returning the result of a 232 * request to a waiting (or potentially timed out) thread. 233 * 234 * @param id The result id of the request as a {@link String}. 235 * @param result The result of a request as a {@link String}. 236 */ 237 @JavascriptInterface 238 @SuppressWarnings("unused") 239 public void onResult(String id, String result) { 240 final long resultId; 241 try { 242 resultId = Long.parseLong(id); 243 } catch (NumberFormatException e) { 244 return; 245 } 246 247 synchronized (mResultLock) { 248 if (resultId > mResultId) { 249 mResult = Boolean.parseBoolean(result); 250 mResultId = resultId; 251 } 252 mResultLock.notifyAll(); 253 } 254 } 255 } 256} 257