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