Home | History | Annotate | Download | only in accessibility
      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.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