JellyBeanAccessibilityInjector.java revision 1320f92c476a1ad9d19dba2a48c72b75566198e9
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
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.Locale;
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.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(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