0

[WebLayer] Add Browser.setDarkModeStrategy API.

This CL adds a Browser.setDarkModeStrategy API that allows embedders to
control how WebLayer renders pages when in dark mode. Note that this
doesn't provide a way for embedders to decouple a page's dark mode with
WebLayer's entirely (e.g. render a page in dark mode if WebLayer itself
is in light, or vice versa). This can be added as a separate setting if
we need it in the future.

Bug: 1136653
Change-Id: I0003f1e095fda3280a840cf794619050b48fd581
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2674202
Commit-Queue: Robbie McElrath <rmcelrath@chromium.org>
Reviewed-by: Scott Violet <sky@chromium.org>
Cr-Commit-Position: refs/heads/master@{#851359}
This commit is contained in:
Robbie McElrath
2021-02-06 00:19:32 +00:00
committed by Chromium LUCI CQ
parent d9881f40ba
commit 61c785eb6c
16 changed files with 306 additions and 13 deletions
weblayer
browser
android
javatests
browser_impl.cc
java
public
shell
test

@ -15,8 +15,9 @@
needed when building test cases. -->
<application>
<activity android:name="org.chromium.test.broker.OnDeviceInstrumentationBroker"
android:exported="true"/>
<activity android:name="org.chromium.weblayer.shell.InstrumentationActivity" />
android:exported="true"/>
<activity android:name="org.chromium.weblayer.shell.InstrumentationActivity"
android:theme="@style/Theme.AppCompat.DayNight" />
</application>
<instrumentation android:name="org.chromium.base.test.BaseChromiumAndroidJUnitRunner"

@ -13,6 +13,7 @@ android_library("weblayer_java_tests") {
"src/org/chromium/weblayer/test/BrowserTest.java",
"src/org/chromium/weblayer/test/CookieManagerTest.java",
"src/org/chromium/weblayer/test/CrashReporterTest.java",
"src/org/chromium/weblayer/test/DarkModeTest.java",
"src/org/chromium/weblayer/test/DataClearingTest.java",
"src/org/chromium/weblayer/test/DisplayCutoutTest.java",
"src/org/chromium/weblayer/test/DowngradeTest.java",
@ -54,6 +55,7 @@ android_library("weblayer_java_tests") {
"//net/android:net_java_test_support",
"//third_party/android_deps:android_support_v4_java",
"//third_party/android_deps:androidx_activity_activity_java",
"//third_party/android_deps:androidx_appcompat_appcompat_java",
"//third_party/android_deps:androidx_core_core_java",
"//third_party/android_deps:androidx_fragment_fragment_java",
"//third_party/android_deps:androidx_test_runner_java",

@ -0,0 +1,104 @@
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.weblayer.test;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.test.filters.SmallTest;
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.chromium.content_public.browser.test.util.TestThreadUtils;
import org.chromium.weblayer.DarkModeStrategy;
import org.chromium.weblayer.shell.InstrumentationActivity;
/**
* Tests that dark mode is handled correctly.
*/
@RunWith(WebLayerJUnit4ClassRunner.class)
public class DarkModeTest {
private InstrumentationActivity mActivity;
@Rule
public InstrumentationActivityTestRule mActivityTestRule =
new InstrumentationActivityTestRule();
private void setDarkModeStrategy(@DarkModeStrategy int darkModeStrategy) {
TestThreadUtils.runOnUiThreadBlocking(() -> {
mActivity.loadWebLayerSync(mActivityTestRule.getContextForWebLayer());
mActivity.getBrowser().setDarkModeStrategy(darkModeStrategy);
});
}
private boolean loadPageAndGetPrefersDark() {
mActivityTestRule.navigateAndWait(mActivityTestRule.getTestDataURL("dark_mode.html"));
return mActivityTestRule.executeScriptAndExtractBoolean(
"window.matchMedia('(prefers-color-scheme: dark)').matches");
}
@Test
@SmallTest
public void testDarkModeWithWebThemeDarkening() throws Exception {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
mActivity = mActivityTestRule.launchShell(new Bundle());
setDarkModeStrategy(DarkModeStrategy.WEB_THEME_DARKENING_ONLY);
boolean prefersDark = loadPageAndGetPrefersDark();
Assert.assertTrue(prefersDark);
}
@Test
@SmallTest
public void testDarkModeWithUserAgentDarkening() throws Exception {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
mActivity = mActivityTestRule.launchShell(new Bundle());
setDarkModeStrategy(DarkModeStrategy.USER_AGENT_DARKENING_ONLY);
boolean prefersDark = loadPageAndGetPrefersDark();
Assert.assertFalse(prefersDark);
}
@Test
@SmallTest
public void testDarkModeWithPreferWebThemeDarkening() throws Exception {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
mActivity = mActivityTestRule.launchShell(new Bundle());
setDarkModeStrategy(DarkModeStrategy.PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING);
boolean prefersDark = loadPageAndGetPrefersDark();
Assert.assertTrue(prefersDark);
}
@Test
@SmallTest
public void testLightModeWithWebThemeDarkening() throws Exception {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
mActivity = mActivityTestRule.launchShell(new Bundle());
setDarkModeStrategy(DarkModeStrategy.WEB_THEME_DARKENING_ONLY);
boolean prefersDark = loadPageAndGetPrefersDark();
Assert.assertFalse(prefersDark);
}
@Test
@SmallTest
public void testLightModeWithUserAgentDarkening() throws Exception {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
mActivity = mActivityTestRule.launchShell(new Bundle());
setDarkModeStrategy(DarkModeStrategy.USER_AGENT_DARKENING_ONLY);
boolean prefersDark = loadPageAndGetPrefersDark();
Assert.assertFalse(prefersDark);
}
@Test
@SmallTest
public void testLightModeWithPreferWebThemeDarkening() throws Exception {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
mActivity = mActivityTestRule.launchShell(new Bundle());
setDarkModeStrategy(DarkModeStrategy.PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING);
boolean prefersDark = loadPageAndGetPrefersDark();
Assert.assertFalse(prefersDark);
}
}

@ -33,9 +33,7 @@
#include "base/json/json_writer.h"
#include "weblayer/browser/browser_process.h"
#include "weblayer/browser/java/jni/BrowserImpl_jni.h"
#endif
#if defined(OS_ANDROID)
using base::android::AttachCurrentThread;
using base::android::JavaParamRef;
using base::android::ScopedJavaLocalRef;
@ -43,6 +41,16 @@ using base::android::ScopedJavaLocalRef;
namespace weblayer {
#if defined(OS_ANDROID)
// This MUST match the values defined in
// org.chromium.weblayer_private.interfaces.DarkModeStrategy.
enum class DarkModeStrategy {
kWebThemeDarkeningOnly = 0,
kUserAgentDarkeningOnly = 1,
kPreferWebThemeOverUserAgentDarkening = 2,
};
#endif
// static
constexpr char BrowserImpl::kPersistenceFilePrefix[];
@ -217,12 +225,41 @@ void BrowserImpl::SetWebPreferences(blink::web_pref::WebPreferences* prefs) {
#if defined(OS_ANDROID)
prefs->password_echo_enabled = Java_BrowserImpl_getPasswordEchoEnabled(
AttachCurrentThread(), java_impl_);
prefs->preferred_color_scheme =
Java_BrowserImpl_getDarkThemeEnabled(AttachCurrentThread(), java_impl_)
? blink::mojom::PreferredColorScheme::kDark
: blink::mojom::PreferredColorScheme::kLight;
prefs->font_scale_factor =
Java_BrowserImpl_getFontScale(AttachCurrentThread(), java_impl_);
bool is_dark =
Java_BrowserImpl_getDarkThemeEnabled(AttachCurrentThread(), java_impl_);
if (is_dark) {
DarkModeStrategy dark_strategy =
static_cast<DarkModeStrategy>(Java_BrowserImpl_getDarkModeStrategy(
AttachCurrentThread(), java_impl_));
switch (dark_strategy) {
case DarkModeStrategy::kPreferWebThemeOverUserAgentDarkening:
// Blink's behavior is that if the preferred color scheme matches the
// browser's color scheme, then force dark will be disabled, otherwise
// the preferred color scheme will be reset to 'light'. Therefore
// when enabling force dark, we also set the preferred color scheme to
// dark so that dark themed content will be preferred over force
// darkening.
prefs->preferred_color_scheme =
blink::mojom::PreferredColorScheme::kDark;
prefs->force_dark_mode_enabled = true;
break;
case DarkModeStrategy::kWebThemeDarkeningOnly:
prefs->preferred_color_scheme =
blink::mojom::PreferredColorScheme::kDark;
prefs->force_dark_mode_enabled = false;
break;
case DarkModeStrategy::kUserAgentDarkeningOnly:
prefs->preferred_color_scheme =
blink::mojom::PreferredColorScheme::kLight;
prefs->force_dark_mode_enabled = true;
break;
}
} else {
prefs->preferred_color_scheme = blink::mojom::PreferredColorScheme::kLight;
prefs->force_dark_mode_enabled = false;
}
#endif
}

@ -464,6 +464,7 @@ android_library("interfaces_java") {
"org/chromium/weblayer_private/interfaces/BrowserFragmentArgs.java",
"org/chromium/weblayer_private/interfaces/BrowsingDataType.java",
"org/chromium/weblayer_private/interfaces/CookieChangeCause.java",
"org/chromium/weblayer_private/interfaces/DarkModeStrategy.java",
"org/chromium/weblayer_private/interfaces/DownloadError.java",
"org/chromium/weblayer_private/interfaces/DownloadState.java",
"org/chromium/weblayer_private/interfaces/GoogleAccountServiceType.java",

@ -25,6 +25,7 @@ import org.chromium.components.embedder_support.view.ContentView;
import org.chromium.ui.base.DeviceFormFactor;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.weblayer_private.interfaces.APICallException;
import org.chromium.weblayer_private.interfaces.DarkModeStrategy;
import org.chromium.weblayer_private.interfaces.IBrowser;
import org.chromium.weblayer_private.interfaces.IBrowserClient;
import org.chromium.weblayer_private.interfaces.IObjectWrapper;
@ -83,6 +84,8 @@ public class BrowserImpl extends IBrowser.Stub implements View.OnAttachStateChan
// Cache the value instead of querying system every time.
private Boolean mPasswordEchoEnabled;
private Boolean mDarkThemeEnabled;
@DarkModeStrategy
private int mDarkModeStrategy = DarkModeStrategy.WEB_THEME_DARKENING_ONLY;
private Float mFontScale;
private boolean mViewAttachedToWindow;
private boolean mNotifyOnBrowserControlsOffsetsChanged;
@ -479,6 +482,20 @@ public class BrowserImpl extends IBrowser.Stub implements View.OnAttachStateChan
tab.destroy();
}
@Override
public void setDarkModeStrategy(@DarkModeStrategy int strategy) {
if (mDarkModeStrategy == strategy) {
return;
}
mDarkModeStrategy = strategy;
BrowserImplJni.get().webPreferencesChanged(mNativeBrowser);
}
@CalledByNative
int getDarkModeStrategy() {
return mDarkModeStrategy;
}
@Override
public IUrlBarController getUrlBarController() {
StrictModeWorkaround.apply();

@ -0,0 +1,19 @@
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.weblayer_private.interfaces;
import androidx.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@IntDef({DarkModeStrategy.PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING,
DarkModeStrategy.WEB_THEME_DARKENING_ONLY, DarkModeStrategy.USER_AGENT_DARKENING_ONLY})
@Retention(RetentionPolicy.SOURCE)
public @interface DarkModeStrategy {
int WEB_THEME_DARKENING_ONLY = 0;
int USER_AGENT_DARKENING_ONLY = 1;
int PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING = 2;
}

@ -48,4 +48,7 @@ interface IBrowser {
// Added in 89.
void setMinimumSurfaceSize(in int width, in int height) = 15;
// Added in 90.
void setDarkModeStrategy(in int strategy) = 16;
}

@ -52,6 +52,7 @@ android_library("java") {
"org/chromium/weblayer/CookieManager.java",
"org/chromium/weblayer/CrashReporterCallback.java",
"org/chromium/weblayer/CrashReporterController.java",
"org/chromium/weblayer/DarkModeStrategy.java",
"org/chromium/weblayer/Download.java",
"org/chromium/weblayer/DownloadCallback.java",
"org/chromium/weblayer/DownloadError.java",

@ -432,6 +432,31 @@ public class Browser {
}
}
/**
* Controls how sites are themed when WebLayer is in dark mode. WebLayer considers itself to be
* in dark mode if the UI_MODE_NIGHT_YES flag of its Resources' Configuration's uiMode field is
* set, which is typically controlled with AppCompatDelegate#setDefaultNightMode. By default
* pages will only be rendered in dark mode if WebLayer is in dark mode and they provide a dark
* theme in CSS. See DarkModeStrategy for other possible configurations.
*
* @see DarkModeStrategy
* @param strategy See {@link DarkModeStrategy}.
*
* @since 90
*/
public void setDarkModeStrategy(@DarkModeStrategy int strategy) {
ThreadCheck.ensureOnUiThread();
if (WebLayer.getSupportedMajorVersionInternal() < 89) {
throw new UnsupportedOperationException();
}
throwIfDestroyed();
try {
mImpl.setDarkModeStrategy(strategy);
} catch (RemoteException e) {
throw new APICallException(e);
}
}
/**
* Returns {@link Profile} associated with this Browser Fragment. Multiple fragments can share
* the same Profile.

@ -0,0 +1,40 @@
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.weblayer;
import androidx.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* @hide
*/
@IntDef({DarkModeStrategy.PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING,
DarkModeStrategy.WEB_THEME_DARKENING_ONLY, DarkModeStrategy.USER_AGENT_DARKENING_ONLY})
@Retention(RetentionPolicy.SOURCE)
public @interface DarkModeStrategy {
/**
* Only render pages in dark mode if they provide a dark theme in their CSS. If no theme is
* provided, the page will render with its default styling, which could be a light theme.
*/
int WEB_THEME_DARKENING_ONLY =
org.chromium.weblayer_private.interfaces.DarkModeStrategy.WEB_THEME_DARKENING_ONLY;
/**
* Always apply automatic user-agent darkening to pages, ignoring any dark theme that the
* site provides. All pages will appear dark in this mode.
*/
int USER_AGENT_DARKENING_ONLY =
org.chromium.weblayer_private.interfaces.DarkModeStrategy.USER_AGENT_DARKENING_ONLY;
/**
* Render pages using their specified dark theme if available, otherwise fall back on automatic
* user-agent darkening. All pages will appear dark in this mode.
*/
int PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING =
org.chromium.weblayer_private.interfaces.DarkModeStrategy
.PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING;
}

@ -29,6 +29,7 @@
</intent-filter>
</activity>
<activity android:name="InstrumentationActivity"
android:theme="@style/Theme.AppCompat.DayNight"
android:windowSoftInputMode="adjustResize">
<!-- Add these intent filters so tests can resolve these intents. -->
<intent-filter>

@ -24,7 +24,21 @@
<item android:id="@+id/toggle_controls_animations_id"
android:checkable="true"
android:title="Animate browser controls changes" />
<item android:id="@+id/toggle_dark_mode"
android:checkable="true"
android:title="Dark Mode" />
<item android:title="Dark Mode">
<menu>
<item android:id="@+id/toggle_dark_mode"
android:checkable="true"
android:title="Enable dark mode" />
<item android:enabled="false"
android:title="Dark mode strategy" />
<group android:checkableBehavior="single">
<item android:id="@+id/dark_mode_web_theme"
android:title="Web theme only" />
<item android:id="@+id/dark_mode_user_agent"
android:title="User agent only" />
<item android:id="@+id/dark_mode_prefer_web_theme"
android:title="Prefer web theme" />
</group>
</menu>
</item>
</menu>

@ -18,8 +18,8 @@ import android.view.WindowManager;
import android.widget.LinearLayout;
import android.widget.RelativeLayout;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
@ -46,7 +46,7 @@ import java.util.List;
*/
// This isn't part of Chrome, so using explicit colors/sizes is ok.
@SuppressWarnings("checkstyle:SetTextColorAndSetTextSizeCheck")
public class InstrumentationActivity extends FragmentActivity {
public class InstrumentationActivity extends AppCompatActivity {
private static final String TAG = "WLInstrumentation";
private static final String KEY_MAIN_VIEW_ID = "mainViewId";

@ -52,6 +52,7 @@ import org.chromium.base.compat.ApiHelperForR;
import org.chromium.weblayer.Browser;
import org.chromium.weblayer.BrowsingDataType;
import org.chromium.weblayer.ContextMenuParams;
import org.chromium.weblayer.DarkModeStrategy;
import org.chromium.weblayer.ErrorPageCallback;
import org.chromium.weblayer.FaviconCallback;
import org.chromium.weblayer.FaviconFetcher;
@ -257,6 +258,7 @@ public class WebLayerShellActivity extends AppCompatActivity {
private boolean mSetDarkMode;
private boolean mInIncognitoMode;
private boolean mEnableAltTopView;
private int mDarkModeStrategy = R.id.dark_mode_web_theme;
@Override
protected void onCreate(final Bundle savedInstanceState) {
@ -406,6 +408,7 @@ public class WebLayerShellActivity extends AppCompatActivity {
.findItem(R.id.toggle_controls_animations_id)
.setChecked(mAnimateControlsChanges);
popup.getMenu().findItem(R.id.toggle_dark_mode).setChecked(mSetDarkMode);
popup.getMenu().findItem(mDarkModeStrategy).setChecked(true);
popup.setOnMenuItemClickListener(item -> {
if (item.getItemId() == R.id.toggle_top_view_id) {
@ -454,6 +457,25 @@ public class WebLayerShellActivity extends AppCompatActivity {
return true;
}
if (item.getItemId() == R.id.dark_mode_web_theme) {
mDarkModeStrategy = R.id.dark_mode_web_theme;
mBrowser.setDarkModeStrategy(DarkModeStrategy.WEB_THEME_DARKENING_ONLY);
return true;
}
if (item.getItemId() == R.id.dark_mode_user_agent) {
mDarkModeStrategy = R.id.dark_mode_user_agent;
mBrowser.setDarkModeStrategy(DarkModeStrategy.USER_AGENT_DARKENING_ONLY);
return true;
}
if (item.getItemId() == R.id.dark_mode_prefer_web_theme) {
mDarkModeStrategy = R.id.dark_mode_prefer_web_theme;
mBrowser.setDarkModeStrategy(
DarkModeStrategy.PREFER_WEB_THEME_OVER_USER_AGENT_DARKENING);
return true;
}
return false;
});
popup.show();

@ -0,0 +1,6 @@
<html>
<head>
<meta name="color-scheme" content="dark light">
</head>
<body></body>
</html>