Add accessibility text contrast setting

b/14624452

Adds a feature which draws all text (in the HW accelerated standard
path) in a high contrast mode. Text is drawn at full alpha, and either
white or black (depending on its original color) with a starkly
contrasted outline beneath it.

Change-Id: I943f624b6367de35367cced3b2a8298f2bc62377
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 2687bcc..1a0aadf 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -3675,6 +3675,14 @@
         public static final String ACCESSIBILITY_SPEAK_PASSWORD = "speak_password";
 
         /**
+         * Whether to draw text with high contrast while in accessibility mode.
+         *
+         * @hide
+         */
+        public static final String ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED =
+                "high_text_contrast_enabled";
+
+        /**
          * If injection of accessibility enhancing JavaScript screen-reader
          * is enabled.
          * <p>
@@ -4641,6 +4649,7 @@
             TOUCH_EXPLORATION_ENABLED,
             ACCESSIBILITY_ENABLED,
             ACCESSIBILITY_SPEAK_PASSWORD,
+            ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED,
             ACCESSIBILITY_CAPTIONING_ENABLED,
             ACCESSIBILITY_CAPTIONING_LOCALE,
             ACCESSIBILITY_CAPTIONING_BACKGROUND_COLOR,
diff --git a/core/java/android/view/GLES20Canvas.java b/core/java/android/view/GLES20Canvas.java
index d7d3c72..910f862 100644
--- a/core/java/android/view/GLES20Canvas.java
+++ b/core/java/android/view/GLES20Canvas.java
@@ -168,7 +168,15 @@
         nSetViewport(mRenderer, width, height);
     }
 
-    private static native void nSetViewport(long renderer, int width, int height);
+    private static native void nSetViewport(long renderer,
+            int width, int height);
+
+    @Override
+    public void setHighContrastText(boolean highContrastText) {
+        nSetHighContrastText(mRenderer, highContrastText);
+    }
+
+    private static native void nSetHighContrastText(long renderer, boolean highContrastText);
 
     @Override
     public int onPreDraw(Rect dirty) {
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index a09a061..15b8b4a4 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -13730,6 +13730,7 @@
             int layerType = getLayerType();
 
             final HardwareCanvas canvas = renderNode.start(width, height);
+            canvas.setHighContrastText(mAttachInfo.mHighContrastText);
 
             try {
                 final HardwareLayer layer = getHardwareLayer();
@@ -19897,6 +19898,11 @@
         boolean mViewScrollChanged;
 
         /**
+         * Set to true if high contrast mode enabled
+         */
+        boolean mHighContrastText;
+
+        /**
          * Global to the view hierarchy used as a temporary for dealing with
          * x/y points in the transparent region computations.
          */
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index 7fab808..25c1b40 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -63,6 +63,7 @@
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener;
+import android.view.accessibility.AccessibilityManager.HighTextContrastChangeListener;
 import android.view.accessibility.AccessibilityNodeInfo;
 import android.view.accessibility.AccessibilityNodeProvider;
 import android.view.accessibility.IAccessibilityInteractionConnection;
@@ -313,6 +314,7 @@
     AccessibilityInteractionController mAccessibilityInteractionController;
 
     AccessibilityInteractionConnectionManager mAccessibilityInteractionConnectionManager;
+    HighContrastTextManager mHighContrastTextManager;
 
     SendWindowContentChangedAccessibilityEvent mSendWindowContentChangedAccessibilityEvent;
 
@@ -370,12 +372,15 @@
         mPreviousTransparentRegion = new Region();
         mFirst = true; // true for the first time the view is added
         mAdded = false;
+        mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this);
         mAccessibilityManager = AccessibilityManager.getInstance(context);
         mAccessibilityInteractionConnectionManager =
             new AccessibilityInteractionConnectionManager();
         mAccessibilityManager.addAccessibilityStateChangeListener(
                 mAccessibilityInteractionConnectionManager);
-        mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this);
+        mHighContrastTextManager = new HighContrastTextManager();
+        mAccessibilityManager.addHighTextContrastStateChangeListener(
+                mHighContrastTextManager);
         mViewConfiguration = ViewConfiguration.get(context);
         mDensity = context.getResources().getDisplayMetrics().densityDpi;
         mNoncompatDensity = context.getResources().getDisplayMetrics().noncompatDensityDpi;
@@ -2889,6 +2894,8 @@
         mAccessibilityInteractionConnectionManager.ensureNoConnection();
         mAccessibilityManager.removeAccessibilityStateChangeListener(
                 mAccessibilityInteractionConnectionManager);
+        mAccessibilityManager.removeHighTextContrastStateChangeListener(
+                mHighContrastTextManager);
         removeSendWindowContentChangedCallback();
 
         destroyHardwareRenderer();
@@ -6560,7 +6567,7 @@
         public void onAccessibilityStateChanged(boolean enabled) {
             if (enabled) {
                 ensureConnection();
-                if (mAttachInfo != null && mAttachInfo.mHasWindowFocus) {
+                if (mAttachInfo.mHasWindowFocus) {
                     mView.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
                     View focusedView = mView.findFocus();
                     if (focusedView != null && focusedView != mView) {
@@ -6574,14 +6581,12 @@
         }
 
         public void ensureConnection() {
-            if (mAttachInfo != null) {
-                final boolean registered =
+            final boolean registered =
                     mAttachInfo.mAccessibilityWindowId != AccessibilityNodeInfo.UNDEFINED_ITEM_ID;
-                if (!registered) {
-                    mAttachInfo.mAccessibilityWindowId =
+            if (!registered) {
+                mAttachInfo.mAccessibilityWindowId =
                         mAccessibilityManager.addAccessibilityInteractionConnection(mWindow,
                                 new AccessibilityInteractionConnection(ViewRootImpl.this));
-                }
             }
         }
 
@@ -6595,6 +6600,19 @@
         }
     }
 
+    final class HighContrastTextManager implements HighTextContrastChangeListener {
+        HighContrastTextManager() {
+            mAttachInfo.mHighContrastText = mAccessibilityManager.isHighTextContrastEnabled();
+        }
+        @Override
+        public void onHighTextContrastStateChanged(boolean enabled) {
+            mAttachInfo.mHighContrastText = enabled;
+
+            // Destroy Displaylists so they can be recreated with high contrast recordings
+            destroyHardwareResources();
+        }
+    }
+
     /**
      * This class is an interface this ViewAncestor provides to the
      * AccessibilityManagerService to the latter can interact with
diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java
index cbc38c6..94e2c0e 100644
--- a/core/java/android/view/accessibility/AccessibilityManager.java
+++ b/core/java/android/view/accessibility/AccessibilityManager.java
@@ -18,6 +18,7 @@
 
 import android.Manifest;
 import android.accessibilityservice.AccessibilityServiceInfo;
+import android.annotation.NonNull;
 import android.content.Context;
 import android.content.pm.PackageManager;
 import android.content.pm.ServiceInfo;
@@ -76,6 +77,9 @@
     public static final int STATE_FLAG_TOUCH_EXPLORATION_ENABLED = 0x00000002;
 
     /** @hide */
+    public static final int STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED = 0x00000004;
+
+    /** @hide */
     public static final int INVERSION_DISABLED = -1;
 
     /** @hide */
@@ -127,13 +131,19 @@
 
     boolean mIsTouchExplorationEnabled;
 
+    boolean mIsHighTextContrastEnabled;
+
     private final CopyOnWriteArrayList<AccessibilityStateChangeListener>
             mAccessibilityStateChangeListeners = new CopyOnWriteArrayList<
                     AccessibilityStateChangeListener>();
 
     private final CopyOnWriteArrayList<TouchExplorationStateChangeListener>
             mTouchExplorationStateChangeListeners = new CopyOnWriteArrayList<
-                    TouchExplorationStateChangeListener>();
+            TouchExplorationStateChangeListener>();
+
+    private final CopyOnWriteArrayList<HighTextContrastChangeListener>
+            mHighTextContrastStateChangeListeners = new CopyOnWriteArrayList<
+            HighTextContrastChangeListener>();
 
     /**
      * Listener for the system accessibility state. To listen for changes to the
@@ -166,6 +176,24 @@
         public void onTouchExplorationStateChanged(boolean enabled);
     }
 
+    /**
+     * Listener for the system high text contrast state. To listen for changes to
+     * the high text contrast state on the device, implement this interface and
+     * register it with the system by calling
+     * {@link #addHighTextContrastStateChangeListener}.
+     *
+     * @hide
+     */
+    public interface HighTextContrastChangeListener {
+
+        /**
+         * Called when the high text contrast enabled state changes.
+         *
+         * @param enabled Whether high text contrast is enabled.
+         */
+        public void onHighTextContrastStateChanged(boolean enabled);
+    }
+
     private final IAccessibilityManagerClient.Stub mClient =
             new IAccessibilityManagerClient.Stub() {
         public void setState(int state) {
@@ -262,6 +290,27 @@
     }
 
     /**
+     * Returns if the high text contrast in the system is enabled.
+     * <p>
+     * <strong>Note:</strong> You need to query this only if you application is
+     * doing its own rendering and does not rely on the platform rendering pipeline.
+     * </p>
+     *
+     * @return True if high text contrast is enabled, false otherwise.
+     *
+     * @hide
+     */
+    public boolean isHighTextContrastEnabled() {
+        synchronized (mLock) {
+            IAccessibilityManager service = getServiceLocked();
+            if (service == null) {
+                return false;
+            }
+            return mIsHighTextContrastEnabled;
+        }
+    }
+
+    /**
      * Sends an {@link AccessibilityEvent}.
      *
      * @param event The event to send.
@@ -434,7 +483,7 @@
      * @return True if successfully registered.
      */
     public boolean addAccessibilityStateChangeListener(
-            AccessibilityStateChangeListener listener) {
+            @NonNull AccessibilityStateChangeListener listener) {
         // Final CopyOnArrayList - no lock needed.
         return mAccessibilityStateChangeListeners.add(listener);
     }
@@ -446,7 +495,7 @@
      * @return True if successfully unregistered.
      */
     public boolean removeAccessibilityStateChangeListener(
-            AccessibilityStateChangeListener listener) {
+            @NonNull AccessibilityStateChangeListener listener) {
         // Final CopyOnArrayList - no lock needed.
         return mAccessibilityStateChangeListeners.remove(listener);
     }
@@ -459,7 +508,7 @@
      * @return True if successfully registered.
      */
     public boolean addTouchExplorationStateChangeListener(
-            TouchExplorationStateChangeListener listener) {
+            @NonNull TouchExplorationStateChangeListener listener) {
         // Final CopyOnArrayList - no lock needed.
         return mTouchExplorationStateChangeListeners.add(listener);
     }
@@ -471,12 +520,41 @@
      * @return True if successfully unregistered.
      */
     public boolean removeTouchExplorationStateChangeListener(
-            TouchExplorationStateChangeListener listener) {
+            @NonNull TouchExplorationStateChangeListener listener) {
         // Final CopyOnArrayList - no lock needed.
         return mTouchExplorationStateChangeListeners.remove(listener);
     }
 
     /**
+     * Registers a {@link HighTextContrastChangeListener} for changes in
+     * the global high text contrast state of the system.
+     *
+     * @param listener The listener.
+     * @return True if successfully registered.
+     *
+     * @hide
+     */
+    public boolean addHighTextContrastStateChangeListener(
+            @NonNull HighTextContrastChangeListener listener) {
+        // Final CopyOnArrayList - no lock needed.
+        return mHighTextContrastStateChangeListeners.add(listener);
+    }
+
+    /**
+     * Unregisters a {@link HighTextContrastChangeListener}.
+     *
+     * @param listener The listener.
+     * @return True if successfully unregistered.
+     *
+     * @hide
+     */
+    public boolean removeHighTextContrastStateChangeListener(
+            @NonNull HighTextContrastChangeListener listener) {
+        // Final CopyOnArrayList - no lock needed.
+        return mHighTextContrastStateChangeListeners.remove(listener);
+    }
+
+    /**
      * Sets the current state and notifies listeners, if necessary.
      *
      * @param stateFlags The state flags.
@@ -485,13 +563,17 @@
         final boolean enabled = (stateFlags & STATE_FLAG_ACCESSIBILITY_ENABLED) != 0;
         final boolean touchExplorationEnabled =
                 (stateFlags & STATE_FLAG_TOUCH_EXPLORATION_ENABLED) != 0;
+        final boolean highTextContrastEnabled =
+                (stateFlags & STATE_FLAG_HIGH_TEXT_CONTRAST_ENABLED) != 0;
 
         final boolean wasEnabled = mIsEnabled;
         final boolean wasTouchExplorationEnabled = mIsTouchExplorationEnabled;
+        final boolean wasHighTextContrastEnabled = mIsHighTextContrastEnabled;
 
         // Ensure listeners get current state from isZzzEnabled() calls.
         mIsEnabled = enabled;
         mIsTouchExplorationEnabled = touchExplorationEnabled;
+        mIsHighTextContrastEnabled = highTextContrastEnabled;
 
         if (wasEnabled != enabled) {
             mHandler.sendEmptyMessage(MyHandler.MSG_NOTIFY_ACCESSIBILITY_STATE_CHANGED);
@@ -500,6 +582,10 @@
         if (wasTouchExplorationEnabled != touchExplorationEnabled) {
             mHandler.sendEmptyMessage(MyHandler.MSG_NOTIFY_EXPLORATION_STATE_CHANGED);
         }
+
+        if (wasHighTextContrastEnabled != highTextContrastEnabled) {
+            mHandler.sendEmptyMessage(MyHandler.MSG_NOTIFY_HIGH_TEXT_CONTRAST_STATE_CHANGED);
+        }
     }
 
     /**
@@ -600,9 +686,25 @@
         }
     }
 
+    /**
+     * Notifies the registered {@link HighTextContrastChangeListener}s.
+     */
+    private void handleNotifyHighTextContrastStateChanged() {
+        final boolean isHighTextContrastEnabled;
+        synchronized (mLock) {
+            isHighTextContrastEnabled = mIsHighTextContrastEnabled;
+        }
+        final int listenerCount = mHighTextContrastStateChangeListeners.size();
+        for (int i = 0; i < listenerCount; i++) {
+            mHighTextContrastStateChangeListeners.get(i)
+                    .onHighTextContrastStateChanged(isHighTextContrastEnabled);
+        }
+    }
+
     private final class MyHandler extends Handler {
         public static final int MSG_NOTIFY_ACCESSIBILITY_STATE_CHANGED = 1;
         public static final int MSG_NOTIFY_EXPLORATION_STATE_CHANGED = 2;
+        public static final int MSG_NOTIFY_HIGH_TEXT_CONTRAST_STATE_CHANGED = 3;
 
         public MyHandler(Looper looper) {
             super(looper, null, false);
@@ -617,7 +719,11 @@
 
                 case MSG_NOTIFY_EXPLORATION_STATE_CHANGED: {
                     handleNotifyTouchExplorationStateChanged();
-                }
+                } break;
+
+                case MSG_NOTIFY_HIGH_TEXT_CONTRAST_STATE_CHANGED: {
+                    handleNotifyHighTextContrastStateChanged();
+                } break;
             }
         }
     }
diff --git a/core/jni/android_view_GLES20Canvas.cpp b/core/jni/android_view_GLES20Canvas.cpp
index c352398..8641e73 100644
--- a/core/jni/android_view_GLES20Canvas.cpp
+++ b/core/jni/android_view_GLES20Canvas.cpp
@@ -100,6 +100,12 @@
     renderer->setViewport(width, height);
 }
 
+static void android_view_GLES20Canvas_setHighContrastText(JNIEnv* env, jobject clazz,
+        jlong rendererPtr, jboolean highContrastText) {
+    DisplayListRenderer* renderer = reinterpret_cast<DisplayListRenderer*>(rendererPtr);
+    renderer->setHighContrastText(highContrastText);
+}
+
 static int android_view_GLES20Canvas_prepare(JNIEnv* env, jobject clazz,
         jlong rendererPtr, jboolean opaque) {
     DisplayListRenderer* renderer = reinterpret_cast<DisplayListRenderer*>(rendererPtr);
@@ -842,6 +848,7 @@
 
     { "nDestroyRenderer",   "(J)V",            (void*) android_view_GLES20Canvas_destroyRenderer },
     { "nSetViewport",       "(JII)V",          (void*) android_view_GLES20Canvas_setViewport },
+    { "nSetHighContrastText","(JZ)V",          (void*) android_view_GLES20Canvas_setHighContrastText },
     { "nPrepare",           "(JZ)I",           (void*) android_view_GLES20Canvas_prepare },
     { "nPrepareDirty",      "(JIIIIZ)I",       (void*) android_view_GLES20Canvas_prepareDirty },
     { "nFinish",            "(J)V",            (void*) android_view_GLES20Canvas_finish },