Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / content / public / android / java / src / org / chromium / content / browser / accessibility / JellyBeanAccessibilityInjector.java
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.getWebContents().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(
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);
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.getWebContents().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 }