Adding accessibility content provider responsible for user script injection. Adding support for injecting accessibility in WebViews with disabled JavaScript.

Change-Id: I1576238a0f855bb54e25d19404b8404d7d0f6037
diff --git a/core/java/android/webkit/AccessibilityInjector.java b/core/java/android/webkit/AccessibilityInjector.java
new file mode 100644
index 0000000..49ddc19
--- /dev/null
+++ b/core/java/android/webkit/AccessibilityInjector.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.webkit;
+
+import android.view.KeyEvent;
+import android.view.accessibility.AccessibilityEvent;
+import android.webkit.WebViewCore.EventHub;
+
+/**
+ * This class injects accessibility into WebViews with disabled JavaScript or
+ * WebViews with enabled JavaScript but for which we have no accessibility
+ * script to inject.
+ */
+class AccessibilityInjector {
+
+    // Handle to the WebView this injector is associated with.
+    private final WebView mWebView;
+
+    /**
+     * Creates a new injector associated with a given VwebView.
+     *
+     * @param webView The associated WebView.
+     */
+    public AccessibilityInjector(WebView webView) {
+        mWebView = webView;
+    }
+
+    /**
+     * Processes a key down <code>event</code>.
+     *
+     * @return True if the event was processed.
+     */
+    public boolean onKeyEvent(KeyEvent event) {
+
+        // as a proof of concept let us do the simplest example
+
+        if (event.getAction() != KeyEvent.ACTION_UP) {
+            return false;
+        }
+
+        int keyCode = event.getKeyCode();
+
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_N:
+                modifySelection("extend", "forward", "sentence");
+                break;
+            case KeyEvent.KEYCODE_P:
+                modifySelection("extend", "backward", "sentence");
+                break;
+        }
+
+        return false;
+    }
+
+    /**
+     * Called when the <code>selectionString</code> has changed.
+     */
+    public void onSelectionStringChange(String selectionString) {
+        // put the selection string in an AccessibilityEvent and send it
+        AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_FOCUSED);
+        event.getText().add(selectionString);
+        mWebView.sendAccessibilityEventUnchecked(event);
+    }
+
+    /**
+     * Modifies the current selection.
+     *
+     * @param alter Specifies how to alter the selection.
+     * @param direction The direction in which to alter the selection.
+     * @param granularity The granularity of the selection modification.
+     */
+    private void modifySelection(String alter, String direction, String granularity) {
+        WebViewCore webViewCore = mWebView.getWebViewCore();
+
+        if (webViewCore == null) {
+            return;
+        }
+
+        WebViewCore.ModifySelectionData data = new WebViewCore.ModifySelectionData();
+        data.mAlter = alter;
+        data.mDirection = direction;
+        data.mGranularity = granularity;
+        webViewCore.sendMessage(EventHub.MODIFY_SELECTION, data);
+    }
+}
diff --git a/core/java/android/webkit/WebView.java b/core/java/android/webkit/WebView.java
index e7517205..9f44564 100644
--- a/core/java/android/webkit/WebView.java
+++ b/core/java/android/webkit/WebView.java
@@ -25,19 +25,13 @@
 import android.content.pm.PackageManager;
 import android.database.DataSetObserver;
 import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.BitmapShader;
 import android.graphics.Canvas;
 import android.graphics.Color;
 import android.graphics.Interpolator;
-import android.graphics.Matrix;
-import android.graphics.Paint;
 import android.graphics.Picture;
 import android.graphics.Point;
 import android.graphics.Rect;
 import android.graphics.RectF;
-import android.graphics.Region;
-import android.graphics.Shader;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.net.http.SslCertificate;
@@ -46,9 +40,11 @@
 import android.os.Message;
 import android.os.ServiceManager;
 import android.os.SystemClock;
+import android.speech.tts.TextToSpeech;
 import android.text.IClipboard;
 import android.text.Selection;
 import android.text.Spannable;
+import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.EventLog;
 import android.util.Log;
@@ -64,6 +60,7 @@
 import android.view.ViewConfiguration;
 import android.view.ViewGroup;
 import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityManager;
 import android.view.animation.AlphaAnimation;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputConnection;
@@ -89,7 +86,6 @@
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
-import java.io.IOException;
 import java.net.URLDecoder;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -487,6 +483,10 @@
     // use the framework's ScaleGestureDetector to handle multi-touch
     private ScaleGestureDetector mScaleDetector;
 
+    // An instance for injecting accessibility in WebViews with disabled
+    // JavaScript or ones for which no accessibility script exists
+    private AccessibilityInjector mAccessibilityInjector;
+
     // the anchor point in the document space where VIEW_SIZE_CHANGED should
     // apply to
     private int mAnchorX;
@@ -545,6 +545,7 @@
     static final int CENTER_FIT_RECT                    = 127;
     static final int REQUEST_KEYBOARD_WITH_SELECTION_MSG_ID = 128;
     static final int SET_SCROLLBAR_MODES                = 129;
+    static final int SELECTION_STRING_CHANGED           = 130;
 
     private static final int FIRST_PACKAGE_MSG_ID = SCROLL_TO_MSG_ID;
     private static final int LAST_PACKAGE_MSG_ID = SET_SCROLLBAR_MODES;
@@ -670,6 +671,19 @@
     private int mHorizontalScrollBarMode = SCROLLBAR_AUTO;
     private int mVerticalScrollBarMode = SCROLLBAR_AUTO;
 
+    // the alias via which accessibility JavaScript interface is exposed
+    private static final String ALIAS_ACCESSIBILITY_JS_INTERFACE = "accessibility";
+
+    // JavaScript to inject the script chooser which will
+    // pick the right script for the current URL
+    private static final String ACCESSIBILITY_SCRIPT_CHOOSER_JAVASCRIPT =
+        "javascript:(function() {" +
+        "    var chooser = document.createElement('script');" +
+        "    chooser.type = 'text/javascript';" +
+        "    chooser.src = 'https://ssl.gstatic.com/accessibility/javascript/android/AndroidScriptChooser.user.js';" +
+        "    document.getElementsByTagName('head')[0].appendChild(chooser);" +
+        "  })();";
+
     // Used to match key downs and key ups
     private boolean mGotKeyDown;
 
@@ -846,7 +860,7 @@
      * @param context A Context object used to access application assets.
      * @param attrs An AttributeSet passed to our parent.
      * @param defStyle The default style resource ID.
-     * @param javascriptInterfaces is a Map of intareface names, as keys, and
+     * @param javascriptInterfaces is a Map of interface names, as keys, and
      * object implementing those interfaces, as values.
      * @hide pending API council approval.
      */
@@ -855,6 +869,13 @@
         super(context, attrs, defStyle);
         init();
 
+        if (AccessibilityManager.getInstance(context).isEnabled()) {
+            if (javascriptInterfaces == null) {
+                javascriptInterfaces = new HashMap<String, Object>();
+            }
+            exposeAccessibilityJavaScriptApi(javascriptInterfaces);
+        }
+
         mCallbackProxy = new CallbackProxy(context, this);
         mViewManager = new ViewManager(this);
         mWebViewCore = new WebViewCore(context, this, mCallbackProxy, javascriptInterfaces);
@@ -923,6 +944,26 @@
         mMaximumFling = configuration.getScaledMaximumFlingVelocity();
     }
 
+    /**
+     * Exposes accessibility APIs to JavaScript by appending them to the JavaScript
+     * interfaces map provided by the WebView client. In case of conflicting
+     * alias with the one of the accessibility API the user specified one wins.
+     *
+     * @param javascriptInterfaces A map with interfaces to be exposed to JavaScript.
+     */
+    private void exposeAccessibilityJavaScriptApi(Map<String, Object> javascriptInterfaces) {
+        if (javascriptInterfaces.containsKey(ALIAS_ACCESSIBILITY_JS_INTERFACE)) {
+            Log.w(LOGTAG, "JavaScript interface mapped to \"" + ALIAS_ACCESSIBILITY_JS_INTERFACE
+                    + "\" overrides the accessibility API JavaScript interface. No accessibility"
+                    + "API will be exposed to JavaScript!");
+            return;
+        }
+
+        // expose the TTS for now ...
+        javascriptInterfaces.put(ALIAS_ACCESSIBILITY_JS_INTERFACE,
+                new TextToSpeech(getContext(), null));
+    }
+
     /* package */void updateDefaultZoomDensity(int zoomDensity) {
         final float density = getContext().getResources().getDisplayMetrics().density
                 * 100 / zoomDensity;
@@ -2799,6 +2840,29 @@
             }
             mPageThatNeedsToSlideTitleBarOffScreen = null;
         }
+
+        injectAccessibilityForUrl(url);
+    }
+
+    /**
+     * This method injects accessibility in the loaded document if accessibility
+     * is enabled. If JavaScript is enabled we try to inject a URL specific script.
+     * If no URL specific script is found or JavaScript is disabled we fallback to
+     * the default {@link AccessibilityInjector} implementation.
+     *
+     * @param url The URL loaded by this {@link WebView}.
+     */
+    private void injectAccessibilityForUrl(String url) {
+        if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+            if (getSettings().getJavaScriptEnabled()) {
+                loadUrl(ACCESSIBILITY_SCRIPT_CHOOSER_JAVASCRIPT);
+            } else if (mAccessibilityInjector == null) {
+                mAccessibilityInjector = new AccessibilityInjector(this);
+            }
+        } else {
+            // it is possible that accessibility was turned off between reloads
+            mAccessibilityInjector = null;
+        }
     }
 
     /**
@@ -3722,8 +3786,10 @@
         // Bubble up the key event if
         // 1. it is a system key; or
         // 2. the host application wants to handle it;
+        // 3. the accessibility injector is present and wants to handle it;
         if (event.isSystem()
-                || mCallbackProxy.uiOverrideKeyEvent(event)) {
+                || mCallbackProxy.uiOverrideKeyEvent(event)
+                || (mAccessibilityInjector != null && mAccessibilityInjector.onKeyEvent(event))) {
             return false;
         }
 
@@ -3867,7 +3933,10 @@
         // Bubble up the key event if
         // 1. it is a system key; or
         // 2. the host application wants to handle it;
-        if (event.isSystem() || mCallbackProxy.uiOverrideKeyEvent(event)) {
+        // 3. the accessibility injector is present and wants to handle it;
+        if (event.isSystem()
+                || mCallbackProxy.uiOverrideKeyEvent(event)
+                || (mAccessibilityInjector != null && mAccessibilityInjector.onKeyEvent(event))) {
             return false;
         }
 
@@ -6721,6 +6790,13 @@
                     mVerticalScrollBarMode = msg.arg2;
                     break;
 
+                case SELECTION_STRING_CHANGED:
+                    if (mAccessibilityInjector != null) {
+                        String selectionString = (String) msg.obj;
+                        mAccessibilityInjector.onSelectionStringChange(selectionString);
+                    }
+                    break;
+
                 default:
                     super.handleMessage(msg);
                     break;
diff --git a/core/java/android/webkit/WebViewCore.java b/core/java/android/webkit/WebViewCore.java
index b4ab4df..f67819e 100644
--- a/core/java/android/webkit/WebViewCore.java
+++ b/core/java/android/webkit/WebViewCore.java
@@ -17,7 +17,6 @@
 package android.webkit;
 
 import android.content.Context;
-import android.content.Intent;
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.database.Cursor;
 import android.graphics.Canvas;
@@ -33,12 +32,10 @@
 import android.os.Looper;
 import android.os.Message;
 import android.os.Process;
-import android.provider.Browser;
 import android.provider.OpenableColumns;
 import android.util.Log;
 import android.util.SparseBooleanArray;
 import android.view.KeyEvent;
-import android.view.SurfaceHolder;
 import android.view.SurfaceView;
 import android.view.View;
 
@@ -576,7 +573,18 @@
     /**
      * Provide WebCore with the previously visted links from the history database
      */
-    private native void  nativeProvideVisitedHistory(String[] history);
+    private native void nativeProvideVisitedHistory(String[] history);
+
+    /**
+     * Modifies the current selection.
+     *
+     * @param alter Specifies how to alter the selection.
+     * @param direction The direction in which to alter the selection.
+     * @param granularity The granularity of the selection modification.
+     *
+     * @return The selection string.
+     */
+    private native String nativeModifySelection(String alter, String direction, String granularity);
 
     // EventHub for processing messages
     private final EventHub mEventHub;
@@ -716,7 +724,11 @@
         boolean mRemember;
     }
 
-
+    static class ModifySelectionData {
+        String mAlter;
+        String mDirection;
+        String mGranularity;
+    }
 
         static final String[] HandlerDebugString = {
             "REQUEST_LABEL", // 97
@@ -863,6 +875,9 @@
         static final int ADD_PACKAGE_NAME = 185;
         static final int REMOVE_PACKAGE_NAME = 186;
 
+        // accessibility support
+        static final int MODIFY_SELECTION = 190;
+
         // private message ids
         private static final int DESTROY =     200;
 
@@ -1236,6 +1251,19 @@
                             nativeSetSelection(msg.arg1, msg.arg2);
                             break;
 
+                        case MODIFY_SELECTION:
+                            ModifySelectionData modifySelectionData =
+                                (ModifySelectionData) msg.obj;
+                            String selectionString = nativeModifySelection(
+                                    modifySelectionData.mAlter,
+                                    modifySelectionData.mDirection,
+                                    modifySelectionData.mGranularity);
+
+                            mWebView.mPrivateHandler.obtainMessage(
+                                    WebView.SELECTION_STRING_CHANGED, selectionString)
+                                    .sendToTarget();
+                            break;
+
                         case LISTBOX_CHOICES:
                             SparseBooleanArray choices = (SparseBooleanArray)
                                     msg.obj;