[WebView Support Library] Provide forceDark support in WebSettingsCompat.

Test: WebView CTS tests were copied and modified accordingly.
Test: ./gradlew webkit:connectedCheck --info -Pandroid.testInstrumentationRunnerArguments.class=androidx.webkit.WebSettingsCompatForceDarkTest
Change-Id: If88e0e25f060edde74528d85876b3b84280d79f3
diff --git a/webkit/src/androidTest/AndroidManifest.xml b/webkit/src/androidTest/AndroidManifest.xml
index 74a2360..2e28ecc 100644
--- a/webkit/src/androidTest/AndroidManifest.xml
+++ b/webkit/src/androidTest/AndroidManifest.xml
@@ -23,12 +23,10 @@
     <!-- Note: we must specify this because our local server uses
          http://localhost URLs (and P defaults to blocking cleartext traffic).
          -->
-
-    <application android:label="WebViewAssetLoaderTestIntegrationTest"
+    <application android:label="AndroidX Webkit Unittests"
                  android:hardwareAccelerated="true"
                  android:usesCleartextTraffic="true">
 
-        <!-- Top-level Activity -->
-        <activity android:name=".WebViewAssetLoaderIntegrationTest$TestActivity"/>
+        <activity android:name=".WebViewTestActivity"/>
     </application>
 </manifest>
diff --git a/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatForceDarkTest.java b/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatForceDarkTest.java
new file mode 100644
index 0000000..d9de9b1
--- /dev/null
+++ b/webkit/src/androidTest/java/androidx/webkit/WebSettingsCompatForceDarkTest.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2019 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 androidx.webkit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.view.ViewGroup;
+import android.webkit.WebView;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@RunWith(AndroidJUnit4.class)
+public class WebSettingsCompatForceDarkTest {
+    // LayoutParams are null until WebView has a parent Activity.
+    // Test testForceDark_rendersDark requires LayoutParams to define
+    // width and height of WebView to capture its bitmap representation.
+    @Rule
+    public final ActivityTestRule<WebViewTestActivity> mActivityRule =
+            new ActivityTestRule<>(WebViewTestActivity.class);
+
+    private WebViewOnUiThread mWebViewOnUiThread;
+
+    @Before
+    public void setUp() {
+        mWebViewOnUiThread = new WebViewOnUiThread(mActivityRule.getActivity().getWebView());
+    }
+
+    @After
+    public void tearDown() {
+        if (mWebViewOnUiThread != null) {
+            mWebViewOnUiThread.cleanUp();
+        }
+    }
+
+    /**
+     * This should remain functionally equivalent to
+     * android.webkit.cts.WebSettingsTest#testForceDark_default. Modifications to this test should
+     * be reflected in that test as necessary. See http://go/modifying-webview-cts.
+     */
+    @Test
+    @SmallTest
+    public void testForceDark_default() throws Throwable {
+        WebkitUtils.checkFeature(WebViewFeature.FORCE_DARK);
+
+        assertEquals("The default force dark state should be AUTO",
+                WebSettingsCompat.getForceDark(mWebViewOnUiThread.getSettings()),
+                WebSettingsCompat.FORCE_DARK_AUTO);
+    }
+
+    /**
+     * This should remain functionally equivalent to
+     * android.webkit.cts.WebSettingsTest#testForceDark_rendersDark. Modifications to this test
+     * should be reflected in that test as necessary. See http://go/modifying-webview-cts.
+     */
+    @Test
+    @SmallTest
+    public void testForceDark_rendersDark() throws Throwable {
+        WebkitUtils.checkFeature(WebViewFeature.FORCE_DARK);
+        setWebViewSize(64, 64);
+        Map<Integer, Integer> histogram;
+        Integer[] colourValues;
+
+        // Loading about:blank into a force-dark-on webview should result in a dark background
+        WebSettingsCompat.setForceDark(
+                mWebViewOnUiThread.getSettings(), WebSettingsCompat.FORCE_DARK_ON);
+        assertEquals("Force dark should have been set to ON",
+                WebSettingsCompat.getForceDark(mWebViewOnUiThread.getSettings()),
+                WebSettingsCompat.FORCE_DARK_ON);
+
+        mWebViewOnUiThread.loadUrlAndWaitForCompletion("about:blank");
+        histogram = getBitmapHistogram(mWebViewOnUiThread.captureBitmap(), 0, 0, 64, 64);
+        assertEquals("Bitmap should have a single colour", histogram.size(), 1);
+        colourValues = histogram.keySet().toArray(new Integer[0]);
+        assertTrue("Bitmap colour should be dark", Color.luminance(colourValues[0]) < 0.5f);
+
+        // Loading about:blank into a force-dark-off webview should result in a light background
+        WebSettingsCompat.setForceDark(
+                mWebViewOnUiThread.getSettings(), WebSettingsCompat.FORCE_DARK_OFF);
+        assertEquals("Force dark should have been set to OFF",
+                WebSettingsCompat.getForceDark(mWebViewOnUiThread.getSettings()),
+                WebSettingsCompat.FORCE_DARK_OFF);
+
+        mWebViewOnUiThread.loadUrlAndWaitForCompletion("about:blank");
+        histogram = getBitmapHistogram(
+                mWebViewOnUiThread.captureBitmap(), 0, 0, 64, 64);
+        assertEquals("Bitmap should have a single colour", histogram.size(), 1);
+        colourValues = histogram.keySet().toArray(new Integer[0]);
+        assertTrue("Bitmap colour should be light",
+                Color.luminance(colourValues[0]) > 0.5f);
+    }
+
+    private void setWebViewSize(final int width, final int height) {
+        WebkitUtils.onMainThreadSync(new Runnable() {
+            @Override
+            public void run() {
+                WebView webView = mWebViewOnUiThread.getWebViewOnCurrentThread();
+                ViewGroup.LayoutParams params = webView.getLayoutParams();
+                params.height = height;
+                params.width = width;
+                webView.setLayoutParams(params);
+            }
+        });
+    }
+
+    private Map<Integer, Integer> getBitmapHistogram(
+            Bitmap bitmap, int x, int y, int width, int height) {
+        Map<Integer, Integer> histogram = new HashMap<>();
+        for (int pixel : getBitmapPixels(bitmap, x, y, width, height)) {
+            histogram.put(pixel, histogram.getOrDefault(pixel, 0) + 1);
+        }
+        return histogram;
+    }
+
+    private  int[] getBitmapPixels(Bitmap bitmap, int x, int y, int width, int height) {
+        int[] pixels = new int[width * height];
+        bitmap.getPixels(pixels, 0, width, x, y, width, height);
+        return pixels;
+    }
+}
diff --git a/webkit/src/androidTest/java/androidx/webkit/WebViewAssetLoaderIntegrationTest.java b/webkit/src/androidTest/java/androidx/webkit/WebViewAssetLoaderIntegrationTest.java
index f4df2f4..a87d4aa 100644
--- a/webkit/src/androidTest/java/androidx/webkit/WebViewAssetLoaderIntegrationTest.java
+++ b/webkit/src/androidTest/java/androidx/webkit/WebViewAssetLoaderIntegrationTest.java
@@ -16,8 +16,6 @@
 
 package androidx.webkit;
 
-import android.app.Activity;
-import android.os.Bundle;
 import android.webkit.WebResourceRequest;
 import android.webkit.WebResourceResponse;
 import android.webkit.WebView;
@@ -38,8 +36,8 @@
     private static final String TAG = "WebViewAssetLoaderIntegrationTest";
 
     @Rule
-    public final ActivityTestRule<TestActivity> mActivityRule =
-                                    new ActivityTestRule<>(TestActivity.class);
+    public final ActivityTestRule<WebViewTestActivity> mActivityRule =
+                                    new ActivityTestRule<>(WebViewTestActivity.class);
 
     private WebViewOnUiThread mOnUiThread;
     private WebViewAssetLoader mAssetLoader;
@@ -65,23 +63,6 @@
         }
     }
 
-    // An Activity for Integeration tests
-    public static class TestActivity extends Activity {
-        private WebView mWebView;
-
-        public WebView getWebView() {
-            return mWebView;
-        }
-
-        // Runs before test suite's @Before.
-        @Override
-        protected void onCreate(Bundle savedInstanceState) {
-            super.onCreate(savedInstanceState);
-            mWebView = new WebView(this);
-            setContentView(mWebView);
-        }
-    }
-
     @Before
     public void setUp() {
         mAssetLoader = (new WebViewAssetLoader.Builder(mActivityRule.getActivity())).build();
@@ -99,7 +80,7 @@
     @Test
     @MediumTest
     public void testAssetHosting() throws Exception {
-        final TestActivity activity = mActivityRule.getActivity();
+        final WebViewTestActivity activity = mActivityRule.getActivity();
 
         String url =
                 mAssetLoader.getAssetsHttpsPrefix().buildUpon()
@@ -116,7 +97,7 @@
     @Test
     @MediumTest
     public void testResourcesHosting() throws Exception {
-        final TestActivity activity = mActivityRule.getActivity();
+        final WebViewTestActivity activity = mActivityRule.getActivity();
 
         String url =
                 mAssetLoader.getResourcesHttpsPrefix().buildUpon()
diff --git a/webkit/src/androidTest/java/androidx/webkit/WebViewOnUiThread.java b/webkit/src/androidTest/java/androidx/webkit/WebViewOnUiThread.java
index 21e687c..522795f 100644
--- a/webkit/src/androidTest/java/androidx/webkit/WebViewOnUiThread.java
+++ b/webkit/src/androidTest/java/androidx/webkit/WebViewOnUiThread.java
@@ -21,6 +21,7 @@
 
 import android.content.Context;
 import android.graphics.Bitmap;
+import android.graphics.Canvas;
 import android.net.Uri;
 import android.os.Looper;
 import android.os.SystemClock;
@@ -33,6 +34,7 @@
 import androidx.annotation.CallSuper;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
+import androidx.concurrent.futures.ResolvableFuture;
 import androidx.test.core.app.ApplicationProvider;
 
 import java.util.concurrent.Callable;
@@ -416,6 +418,41 @@
     }
 
     /**
+     * Wait for the current state of the DOM to be ready to render on the next draw.
+     */
+    public void waitForDOMReadyToRender() {
+        final ResolvableFuture<Void> future = ResolvableFuture.create();
+        postVisualStateCallbackCompat(0, new WebViewCompat.VisualStateCallback() {
+            @Override
+            public void onComplete(long requestId) {
+                future.set(null);
+            }
+        });
+        WebkitUtils.waitForFuture(future);
+    }
+
+    /**
+     * Capture a bitmap representation of the current WebView state.
+     *
+     * This synchronises so that the bitmap contents reflects the current DOM state, rather than
+     * potentially capturing a previously generated frame.
+     */
+    public Bitmap captureBitmap() {
+        getSettings().setOffscreenPreRaster(true);
+        waitForDOMReadyToRender();
+        return WebkitUtils.onMainThreadSync(new Callable<Bitmap>() {
+            @Override
+            public Bitmap call() throws Exception {
+                Bitmap bitmap = Bitmap.createBitmap(mWebView.getWidth(), mWebView.getHeight(),
+                        Bitmap.Config.ARGB_8888);
+                Canvas canvas = new Canvas(bitmap);
+                mWebView.draw(canvas);
+                return bitmap;
+            }
+        });
+    }
+
+    /**
      * Returns true if the current thread is the UI thread based on the
      * Looper.
      */
diff --git a/webkit/src/androidTest/java/androidx/webkit/WebViewTestActivity.java b/webkit/src/androidTest/java/androidx/webkit/WebViewTestActivity.java
new file mode 100644
index 0000000..1ddfcf1
--- /dev/null
+++ b/webkit/src/androidTest/java/androidx/webkit/WebViewTestActivity.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2019 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 androidx.webkit;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.webkit.WebView;
+
+public class WebViewTestActivity extends Activity {
+    private WebView mWebView;
+
+    public WebView getWebView() {
+        return mWebView;
+    }
+
+    @Override
+    protected void onCreate(Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+        mWebView = new WebView(this);
+        setContentView(mWebView);
+    }
+}
diff --git a/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java b/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
index 21835fb..b794131 100644
--- a/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
+++ b/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
@@ -282,7 +282,116 @@
         }
     }
 
+    /**
+     * Disable force dark, irrespective of the force dark mode of the WebView parent. In this mode,
+     * WebView content will always be rendered as-is, regardless of whether native views are being
+     * automatically darkened.
+     *
+     * @see #setForceDark
+     * TODO(amalova): unhide
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public static final int FORCE_DARK_OFF = 0;
 
+    /**
+     * Enable force dark dependent on the state of the WebView parent view. If the WebView parent
+     * view is being automatically force darkened
+     * (@see android.view.View#setForceDarkAllowed), then WebView content will be rendered
+     * so as to emulate a dark theme. WebViews that are not attached to the view hierarchy will not
+     * be inverted.
+     *
+     * @see #setForceDark
+     * TODO(amalova): unhide
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public static final int FORCE_DARK_AUTO = 1;
+
+    /**
+     * Used with {@link #setForceDark}
+     *
+     * Unconditionally enable force dark. In this mode WebView content will always be rendered so
+     * as to emulate a dark theme.
+     *
+     * @see #setForceDark
+     * TODO(amalova): unhide
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public static final int FORCE_DARK_ON = 2;
+
+    /**
+     * @hide
+     */
+    // TODO(amalova): redefine with framework constants when AndroidX compiles against Q
+    @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
+    @IntDef(value = {
+            FORCE_DARK_OFF,
+            FORCE_DARK_AUTO,
+            FORCE_DARK_ON,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @Target({ElementType.PARAMETER, ElementType.METHOD})
+    public @interface ForceDark {}
+
+    /**
+     * Set the force dark mode for this WebView.
+     *
+     * <p>
+     * This method should only be called if
+     * {@link WebViewFeature#isFeatureSupported(String)}
+     * returns true for {@link WebViewFeature#FORCE_DARK}.
+     *
+     * @param forceDarkMode the force dark mode to set.
+     *
+     * TODO(amalova): unhide
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @SuppressLint("NewApi")
+    @RequiresFeature(name = WebViewFeature.FORCE_DARK,
+            enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+    public static void setForceDark(WebSettings webSettings, @ForceDark int forceDarkMode) {
+        WebViewFeatureInternal webViewFeature =
+                WebViewFeatureInternal.getFeature(WebViewFeature.FORCE_DARK);
+        if (webViewFeature.isSupportedByWebView()) {
+            getAdapter(webSettings).setForceDark(forceDarkMode);
+        } else {
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
+        }
+    }
+
+    /**
+     * Get the force dark mode for this WebView.
+     *
+     * <p>
+     * The default force dark mode is {@link #FORCE_DARK_AUTO}
+     *
+     * <p>
+     * This method should only be called if
+     * {@link WebViewFeature#isFeatureSupported(String)}
+     * returns true for {@link WebViewFeature#FORCE_DARK}.
+     *
+     * @return the currently set force dark mode.
+     *
+     * TODO(amalova): unhide
+     * @hide
+     */
+    // TODO(amalova): add support with the framework APIs when AndroidX compiles against the Q SDK
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    @SuppressLint("NewApi")
+    @RequiresFeature(name = WebViewFeature.FORCE_DARK,
+            enforcement = "androidx.webkit.WebViewFeature#isFeatureSupported")
+    public static @ForceDark int getForceDark(WebSettings webSettings) {
+        WebViewFeatureInternal webViewFeature =
+                WebViewFeatureInternal.getFeature(WebViewFeature.FORCE_DARK);
+        if (webViewFeature.isSupportedByWebView()) {
+            return getAdapter(webSettings).getForceDark();
+        } else {
+            throw WebViewFeatureInternal.getUnsupportedOperationException();
+        }
+    }
 
     private static WebSettingsAdapter getAdapter(WebSettings webSettings) {
         return WebViewGlueCommunicator.getCompatConverter().convertSettings(webSettings);
diff --git a/webkit/src/main/java/androidx/webkit/WebViewFeature.java b/webkit/src/main/java/androidx/webkit/WebViewFeature.java
index 02f0fd7..5994789 100644
--- a/webkit/src/main/java/androidx/webkit/WebViewFeature.java
+++ b/webkit/src/main/java/androidx/webkit/WebViewFeature.java
@@ -88,6 +88,7 @@
             PROXY_OVERRIDE,
             SUPPRESS_ERROR_PAGE,
             MULTI_PROCESS_QUERY,
+            FORCE_DARK,
     })
     @Retention(RetentionPolicy.SOURCE)
     @Target({ElementType.PARAMETER, ElementType.METHOD})
@@ -404,6 +405,18 @@
     public static final String MULTI_PROCESS_QUERY = "MULTI_PROCESS_QUERY";
 
     /**
+     * Feature for {@link #isFeatureSupported(String)}.
+     * This feature covers
+     * {@link WebViewCompat#setForceDark(WebSettings, int)} and
+     * {@link WebViewCompat#getForceDark(WebSettings)}.
+     *
+     * TODO(amalova): unhide
+     * @hide
+     */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    public static final String FORCE_DARK = "FORCE_DARK";
+
+    /**
      * Return whether a feature is supported at run-time. On devices running Android version {@link
      * android.os.Build.VERSION_CODES#LOLLIPOP} and higher, this will check whether a feature is
      * supported, depending on the combination of the desired feature, the Android version of
diff --git a/webkit/src/main/java/androidx/webkit/internal/WebSettingsAdapter.java b/webkit/src/main/java/androidx/webkit/internal/WebSettingsAdapter.java
index 672a594..36ca0a1 100644
--- a/webkit/src/main/java/androidx/webkit/internal/WebSettingsAdapter.java
+++ b/webkit/src/main/java/androidx/webkit/internal/WebSettingsAdapter.java
@@ -86,4 +86,17 @@
         return mBoundaryInterface.getWillSuppressErrorPage();
     }
 
+    /**
+     * Adapter method for {@link androidx.webkit.WebSettingsCompat#setForceDark}.
+     */
+    public void setForceDark(int forceDarkMode) {
+        mBoundaryInterface.setForceDark(forceDarkMode);
+    }
+
+    /**
+     * Adapter method for {@link androidx.webkit.WebSettingsCompat#getForceDark}.
+     */
+    public int getForceDark() {
+        return mBoundaryInterface.getForceDark();
+    }
 }
diff --git a/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java b/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
index 975cd20..4c46071 100644
--- a/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
+++ b/webkit/src/main/java/androidx/webkit/internal/WebViewFeatureInternal.java
@@ -332,6 +332,13 @@
      */
     MULTI_PROCESS_QUERY(WebViewFeature.MULTI_PROCESS_QUERY, Features.MULTI_PROCESS_QUERY),
 
+    /**
+     * This feature covers
+     * {@link androidx.webkit.WebSettingsCompat#setForceDark(WebSettings, int)} and
+     * {@link androidx.webkit.WebSettingsCompat#getForceDark(WebSettings)}.
+     */
+    FORCE_DARK(WebViewFeature.FORCE_DARK, Features.FORCE_DARK),
+
     ;  // This semicolon ends the enum. Add new features with a trailing comma above this line.
 
     private static final int NOT_SUPPORTED_BY_FRAMEWORK = -1;