[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;