0

dark_mode: Add educational nudge

This cl adds educational nudge for dark/light mode.
The nudge will be shown at most 3 times per user. And will not be
shown any more if the user toggled "Dark theme" inside quick settings
or personalization hub to change the color mode, which means users
already know how to change the color mode.

Bug: 1317858
Test: Added tests
Change-Id: I7f183b741566dc2f062ff9d9af0d4a08bec1504d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3641488
Commit-Queue: Min Chen <minch@chromium.org>
Reviewed-by: Ahmed Fakhry <afakhry@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1002915}
This commit is contained in:
minch
2022-05-12 23:20:11 +00:00
committed by Chromium LUCI CQ
parent fb37a4fb54
commit a0c18b94ff
17 changed files with 394 additions and 8 deletions

@ -956,6 +956,10 @@ component("ash") {
"style/ash_color_provider.h",
"style/close_button.cc",
"style/close_button.h",
"style/dark_light_mode_nudge.cc",
"style/dark_light_mode_nudge.h",
"style/dark_light_mode_nudge_controller.cc",
"style/dark_light_mode_nudge_controller.h",
"style/dark_mode_controller.cc",
"style/dark_mode_controller.h",
"style/default_color_constants.h",
@ -2661,6 +2665,7 @@ test("ash_unittests") {
"shelf/test/widget_animation_smoothness_inspector.h",
"shell_unittest.cc",
"style/ash_color_provider_unittests.cc",
"style/dark_light_mode_nudge_controller_unittests.cc",
"system/accessibility/accessibility_feature_pod_controller_unittest.cc",
"system/accessibility/autoclick_menu_bubble_controller_unittest.cc",
"system/accessibility/dictation_bubble_controller_unittest.cc",

@ -476,6 +476,12 @@ This file contains the strings for ash.
<message name="IDS_ASH_STATUS_TRAY_DARK_THEME_OFF_STATE_AUTO_SCHEDULED" desc="Button label for the Dark theme feature when auto scheduling is enabled." meaning="Dark mode is automatically scheduled to be off until sunset. [CHAR_LIMIT=16]">
Off until sunset
</message>
<message name="IDS_ASH_DARK_LIGHT_MODE_EDUCATIONAL_NUDGE_IN_CLAMSHELL_MODE" desc="The label used for the dark light mode educational nudge in clamshell mode.">
Switch between dark and light theme. Right-click on the desktop and select Wallpaper &#0038; style.
</message>
<message name="IDS_ASH_DARK_LIGHT_MODE_EDUCATIONAL_NUDGE_IN_TABLET_MODE" desc="The label used for the dark light mode educational nudge in tablet mode.">
Switch between dark and light theme. Touch and hold on the desktop, then select Wallpaper &#0038; style.
</message>
<message name="IDS_ASH_STATUS_TRAY_CAST_CAST_DESKTOP_ACCESSIBILITY_STOP" desc="Stop button accessibility label used in the tray popup to tell the user to stop a cast to the desktop.">
Stop casting screen to <ph name="RECEIVER_NAME">$1<ex>Living Room</ex></ph>
</message>

@ -0,0 +1 @@
54285df1917772b77f8f21a9e28e4656d435058d

@ -0,0 +1 @@
7afea6cc48925a1b3af82946a48156aa6a5fd86a

@ -73,6 +73,9 @@ constexpr bool kDefaultDarkModeEnabled = false;
// Whether color mode is themed by default.
constexpr bool kDefaultColorModeThemed = true;
// Maximum number of times that dark/light mode educational nudge can be shown.
constexpr int kDarkLightModeNudgeMaxShownCount = 3;
// The default delay before a held keypress will start to auto repeat.
constexpr base::TimeDelta kDefaultKeyAutoRepeatDelay = base::Milliseconds(500);

@ -489,6 +489,14 @@ const char kColorModeThemed[] = "ash.dark_mode.color_mode_themed";
// A boolean pref that indicates whether dark mode is enabled.
const char kDarkModeEnabled[] = "ash.dark_mode.enabled";
// An integer pref storing the number of times that dark/light mode educational
// can still be shown. It will be initialized to the maximum number of times
// that the nudge can be shown. And will be set to 0 if the user toggled the
// entry points of dark/light mode ("Dark theme" inside quick settings or
// personalization hub), which means the user already knows how to change the
// color mode of the system.
const char kDarkLightModeNudge[] = "ash.dark_light_mode.educational_nudge";
// An integer pref storing the type of automatic scheduling of turning on and
// off the dark mode feature similar to `kNightLightScheduleType`, but
// custom scheduling (2) is the same as sunset to sunrise scheduling (1)

@ -241,6 +241,7 @@ extern const char kMessageCenterLockScreenModeHideSensitive[];
COMPONENT_EXPORT(ASH_CONSTANTS) extern const char kAmbientColorEnabled[];
COMPONENT_EXPORT(ASH_CONSTANTS) extern const char kColorModeThemed[];
COMPONENT_EXPORT(ASH_CONSTANTS) extern const char kDarkModeEnabled[];
COMPONENT_EXPORT(ASH_CONSTANTS) extern const char kDarkLightModeNudge[];
COMPONENT_EXPORT(ASH_CONSTANTS) extern const char kDarkModeScheduleType[];
COMPONENT_EXPORT(ASH_CONSTANTS) extern const char kNightLightEnabled[];
COMPONENT_EXPORT(ASH_CONSTANTS) extern const char kNightLightTemperature[];

@ -15,6 +15,7 @@
#include "ash/public/cpp/style/color_mode_observer.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/style/dark_mode_controller.h"
#include "ash/wallpaper/wallpaper_controller_impl.h"
#include "base/bind.h"
#include "base/callback_helpers.h"
@ -176,6 +177,9 @@ SkColor AshColorProvider::GetSecondToneColor(SkColor color_of_first_tone) {
std::round(SkColorGetA(color_of_first_tone) * kSecondToneOpacity));
}
// TODO(minch): Moving prefs related logic to DarkModeController instead. To
// keep AshColorProvider only a provider of colors. This will benefit its
// migration to ui/color/color_provider as well (crbug/1292244).
// static
void AshColorProvider::RegisterProfilePrefs(PrefRegistrySimple* registry) {
registry->RegisterBooleanPref(prefs::kDarkModeEnabled,
@ -378,6 +382,8 @@ void AshColorProvider::ToggleColorMode() {
!IsDarkModeEnabled());
active_user_pref_service_->CommitPendingWrite();
NotifyDarkModeEnabledPrefChange();
DarkModeController::Get()->ToggledByUser();
}
void AshColorProvider::UpdateColorModeThemed(bool is_themed) {

@ -2,10 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ash/login/ui/login_test_base.h"
#include "ash/public/cpp/login_types.h"
#include "ash/style/ash_color_provider.h"
#include "ash/login/ui/login_test_base.h"
#include "ash/public/cpp/login_types.h"
#include "ash/session/test_session_controller_client.h"
#include "ash/test/ash_test_base.h"
#include "base/test/scoped_feature_list.h"

@ -0,0 +1,71 @@
// Copyright 2022 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.
#include "ash/style/dark_light_mode_nudge.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_provider.h"
#include "ash/wm/tablet_mode/tablet_mode_controller.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/compositor/layer.h"
#include "ui/views/controls/label.h"
namespace ash {
namespace {
// The size of the dark light mode icon.
constexpr int kIconSize = 20;
// The minimum width of the label.
constexpr int kMinLabelWidth = 200;
// The spacing between the icon and label in the nudge view.
constexpr int kIconLabelSpacing = 16;
// The padding which separates the nudge's border with its inner contents.
constexpr int kNudgePadding = 16;
constexpr char kDarkLightModeNudgeName[] = "DarkLightModeEducationalNudge";
} // namespace
DarkLightModeNudge::DarkLightModeNudge()
: SystemNudge(kDarkLightModeNudgeName,
kIconSize,
kIconLabelSpacing,
kNudgePadding) {}
DarkLightModeNudge::~DarkLightModeNudge() = default;
std::unique_ptr<views::View> DarkLightModeNudge::CreateLabelView() const {
std::unique_ptr<views::Label> label = std::make_unique<views::Label>();
label->SetPaintToLayer();
label->layer()->SetFillsBoundsOpaquely(false);
label->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_LEFT);
label->SetEnabledColor(AshColorProvider::Get()->GetContentLayerColor(
AshColorProvider::ContentLayerType::kTextColorPrimary));
label->SetAutoColorReadabilityEnabled(false);
label->SetText(GetAccessibilityText());
label->SetMultiLine(true);
label->SizeToFit(kMinLabelWidth);
return std::move(label);
}
const gfx::VectorIcon& DarkLightModeNudge::GetIcon() const {
return kUnifiedMenuDarkModeIcon;
}
std::u16string DarkLightModeNudge::GetAccessibilityText() const {
return l10n_util::GetStringUTF16(
TabletMode::Get()->InTabletMode()
? IDS_ASH_DARK_LIGHT_MODE_EDUCATIONAL_NUDGE_IN_TABLET_MODE
: IDS_ASH_DARK_LIGHT_MODE_EDUCATIONAL_NUDGE_IN_CLAMSHELL_MODE);
}
} // namespace ash

@ -0,0 +1,29 @@
// Copyright 2022 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.
#ifndef ASH_STYLE_DARK_LIGHT_MODE_NUDGE_H_
#define ASH_STYLE_DARK_LIGHT_MODE_NUDGE_H_
#include "ash/system/tray/system_nudge.h"
namespace ash {
// Implements an educational nudge for dark light mode.
class DarkLightModeNudge : public SystemNudge {
public:
DarkLightModeNudge();
DarkLightModeNudge(const DarkLightModeNudge&) = delete;
DarkLightModeNudge& operator=(const DarkLightModeNudge&) = delete;
~DarkLightModeNudge() override;
protected:
// SystemNudge:
std::unique_ptr<views::View> CreateLabelView() const override;
const gfx::VectorIcon& GetIcon() const override;
std::u16string GetAccessibilityText() const override;
};
} // namespace ash
#endif // ASH_STYLE_DARK_LIGHT_MODE_NUDGE_H_

@ -0,0 +1,86 @@
// Copyright 2022 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.
#include "ash/style/dark_light_mode_nudge_controller.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/session/session_controller_impl.h"
#include "ash/shell.h"
#include "ash/style/dark_light_mode_nudge.h"
#include "ash/style/dark_mode_controller.h"
#include "chromeos/constants/chromeos_features.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/scoped_user_pref_update.h"
namespace ash {
namespace {
PrefService* GetActiveUserPrefService() {
return DarkModeController::Get()->active_user_pref_service();
}
void SetRemainingShownCount(int count) {
PrefService* prefs = GetActiveUserPrefService();
if (prefs)
prefs->SetInteger(prefs::kDarkLightModeNudge, count);
}
} // namespace
DarkLightModeNudgeController::DarkLightModeNudgeController() = default;
DarkLightModeNudgeController::~DarkLightModeNudgeController() = default;
// static
int DarkLightModeNudgeController::GetRemainingShownCount() {
const PrefService* prefs = GetActiveUserPrefService();
return prefs ? prefs->GetInteger(prefs::kDarkLightModeNudge) : 0;
}
void DarkLightModeNudgeController::MaybeShowNudge() {
if (!ShouldShowNudge())
return;
const int shown_count = GetRemainingShownCount();
ShowNudge();
SetRemainingShownCount(shown_count - 1);
}
void DarkLightModeNudgeController::ToggledByUser() {
SetRemainingShownCount(0);
}
std::unique_ptr<SystemNudge> DarkLightModeNudgeController::CreateSystemNudge() {
return std::make_unique<DarkLightModeNudge>();
}
bool DarkLightModeNudgeController::ShouldShowNudge() const {
if (!chromeos::features::IsDarkLightModeEnabled())
return false;
absl::optional<user_manager::UserType> user_type =
Shell::Get()->session_controller()->GetUserType();
// This can only be called while a user is logged in, so `user_type` should
// never be empty.
DCHECK(user_type);
switch (*user_type) {
case user_manager::USER_TYPE_REGULAR:
case user_manager::USER_TYPE_CHILD:
// We only allow regular and child accounts to see the nudge.
break;
case user_manager::USER_TYPE_GUEST:
case user_manager::USER_TYPE_PUBLIC_ACCOUNT:
case user_manager::USER_TYPE_KIOSK_APP:
case user_manager::USER_TYPE_ARC_KIOSK_APP:
case user_manager::USER_TYPE_WEB_KIOSK_APP:
case user_manager::USER_TYPE_ACTIVE_DIRECTORY:
case user_manager::NUM_USER_TYPES:
return false;
}
return GetRemainingShownCount() > 0;
}
} // namespace ash

@ -0,0 +1,47 @@
// Copyright 2022 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.
#ifndef ASH_STYLE_DARK_LIGHT_MODE_NUDGE_CONTROLLER_H_
#define ASH_STYLE_DARK_LIGHT_MODE_NUDGE_CONTROLLER_H_
#include "ash/ash_export.h"
#include "ash/system/tray/system_nudge_controller.h"
namespace ash {
class SystemNudge;
// Controls the showing and hiding of the dark/light mode educational nudge.
// SystemNudgeController will control the animation, time duration etc of the
// nudge.
class ASH_EXPORT DarkLightModeNudgeController : public SystemNudgeController {
public:
DarkLightModeNudgeController();
DarkLightModeNudgeController(const DarkLightModeNudgeController&) = delete;
DarkLightModeNudgeController& operator=(const DarkLightModeNudgeController&) =
delete;
~DarkLightModeNudgeController() override;
// Gets the remaining number of times that the educational nudge can be shown.
static int GetRemainingShownCount();
// If possible, this will show the nudge that educates the user how to switch
// between the dark and light mode.
void MaybeShowNudge();
// Called when the feature's state is toggled manually by the user.
void ToggledByUser();
protected:
// SystemNudgeController:
std::unique_ptr<SystemNudge> CreateSystemNudge() override;
private:
// Returns true if the educational nudge should be shown.
bool ShouldShowNudge() const;
};
} // namespace ash
#endif // ASH_STYLE_DARK_LIGHT_MODE_NUDGE_CONTROLLER_H_

@ -0,0 +1,92 @@
// Copyright 2022 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.
#include "ash/style/dark_light_mode_nudge_controller.h"
#include "ash/constants/ash_constants.h"
#include "ash/system/dark_mode/dark_mode_feature_pod_controller.h"
#include "ash/system/unified/unified_system_tray.h"
#include "ash/system/unified/unified_system_tray_bubble.h"
#include "ash/test/ash_test_base.h"
#include "base/test/scoped_feature_list.h"
#include "chromeos/constants/chromeos_features.h"
namespace ash {
namespace {
const char kUser[] = "user@gmail.com";
const AccountId account_id = AccountId::FromUserEmailGaiaId(kUser, kUser);
} // namespace
class DarkLightModeNudgeControllerTest : public NoSessionAshTestBase {
public:
DarkLightModeNudgeControllerTest() {
scoped_feature_list_.InitAndEnableFeature(
chromeos::features::kDarkLightMode);
}
DarkLightModeNudgeControllerTest(const DarkLightModeNudgeControllerTest&) =
delete;
DarkLightModeNudgeControllerTest& operator=(
const DarkLightModeNudgeControllerTest&) = delete;
~DarkLightModeNudgeControllerTest() override = default;
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
TEST_F(DarkLightModeNudgeControllerTest, NoNudgeInGuestSession) {
SimulateGuestLogin();
EXPECT_EQ(kDarkLightModeNudgeMaxShownCount,
DarkLightModeNudgeController::GetRemainingShownCount());
}
TEST_F(DarkLightModeNudgeControllerTest, NoNudgeInLockScreen) {
SimulateUserLogin(account_id);
EXPECT_EQ(kDarkLightModeNudgeMaxShownCount - 1,
DarkLightModeNudgeController::GetRemainingShownCount());
// Switch to lock screen should not show the nudge again.
GetSessionControllerClient()->LockScreen();
EXPECT_EQ(kDarkLightModeNudgeMaxShownCount - 1,
DarkLightModeNudgeController::GetRemainingShownCount());
}
TEST_F(DarkLightModeNudgeControllerTest, NudgeShownCount) {
// Login `kDarkLightModeNudgeMaxShownCount` times and verity the remaining
// nudge shown count.
for (int i = kDarkLightModeNudgeMaxShownCount; i > 0; i--) {
SimulateUserLogin(account_id);
EXPECT_EQ(i - 1, DarkLightModeNudgeController::GetRemainingShownCount());
ClearLogin();
}
// The remaining nudge shown count should be 0 after
// `kDarkLightModeNudgeMaxShownCount` times login, which means the nudge will
// not be shown again in next time login.
EXPECT_EQ(0, DarkLightModeNudgeController::GetRemainingShownCount());
}
TEST_F(DarkLightModeNudgeControllerTest, NoNudgeAfterColorModeToggled) {
SimulateUserLogin(account_id);
UnifiedSystemTray* system_tray = GetPrimaryUnifiedSystemTray();
system_tray->ShowBubble();
std::unique_ptr<DarkModeFeaturePodController>
dark_mode_feature_pod_controller =
std::make_unique<DarkModeFeaturePodController>(
system_tray->bubble()->unified_system_tray_controller());
EXPECT_EQ(kDarkLightModeNudgeMaxShownCount - 1,
DarkLightModeNudgeController::GetRemainingShownCount());
EXPECT_GT(DarkLightModeNudgeController::GetRemainingShownCount(), 0);
// Toggle the "Dark theme" feature pod button inside quick settings.
dark_mode_feature_pod_controller->CreateButton();
dark_mode_feature_pod_controller->OnIconPressed();
// The remaining nudge shown count should be 0 after toggling the "Dark theme"
// feature pod button to switch color mode. Even though the nudge hasn't been
// shown `kDarkLightModeNudgeMaxShownCount` yet.
EXPECT_EQ(0, DarkLightModeNudgeController::GetRemainingShownCount());
}
} // namespace ash

@ -4,7 +4,10 @@
#include "ash/style/dark_mode_controller.h"
#include "ash/constants/ash_constants.h"
#include "ash/constants/ash_pref_names.h"
#include "ash/style/dark_light_mode_nudge_controller.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
namespace ash {
@ -19,7 +22,8 @@ DarkModeController::DarkModeController()
: ScheduledFeature(prefs::kDarkModeEnabled,
prefs::kDarkModeScheduleType,
std::string(),
std::string()) {
std::string()),
nudge_controller_(std::make_unique<DarkLightModeNudgeController>()) {
DCHECK(!g_instance);
g_instance = this;
}
@ -40,6 +44,9 @@ void DarkModeController::RegisterProfilePrefs(PrefRegistrySimple* registry) {
registry->RegisterIntegerPref(
prefs::kDarkModeScheduleType,
static_cast<int>(ScheduledFeature::ScheduleType::kSunsetToSunrise));
registry->RegisterIntegerPref(prefs::kDarkLightModeNudge,
kDarkLightModeNudgeMaxShownCount);
}
void DarkModeController::SetAutoScheduleEnabled(bool enabled) {
@ -54,8 +61,18 @@ bool DarkModeController::GetAutoScheduleEnabled() const {
return type == ScheduledFeature::ScheduleType::kSunsetToSunrise;
}
void DarkModeController::ToggledByUser() {
nudge_controller_->ToggledByUser();
}
void DarkModeController::RefreshFeatureState() {}
void DarkModeController::OnSessionStateChanged(
session_manager::SessionState state) {
if (state == session_manager::SessionState::ACTIVE)
nudge_controller_->MaybeShowNudge();
}
const char* DarkModeController::GetFeatureName() const {
return "DarkModeController";
}

@ -7,15 +7,18 @@
#include "ash/ash_export.h"
#include "ash/system/scheduled_feature/scheduled_feature.h"
#include "components/prefs/pref_registry_simple.h"
class PrefRegistrySimple;
namespace ash {
// DarkModeController handles automatic scheduling of dark mode to turn it on
// at sunset and off at sunrise. However, it does not support custom start
// and end times for scheduling.
class DarkLightModeNudgeController;
// TODO(minch): Rename to DarkLightModeController.
// Controls the behavior of dark/light mode. Turns on the dark mode at sunset
// and off at sunrise if auto schedule is set (custom start and end for
// scheduling is not supported). And determine whether to show the educational
// nudge for users on login.
class ASH_EXPORT DarkModeController : public ScheduledFeature {
public:
DarkModeController();
@ -36,15 +39,22 @@ class ASH_EXPORT DarkModeController : public ScheduledFeature {
// at sunrise.
bool GetAutoScheduleEnabled() const;
// Happens if the user toggled the entry points of dark/light mode to switch
// color mode. Educational nudge will not be shown any more when this happens.
void ToggledByUser();
protected:
// ScheduledFeature:
void RefreshFeatureState() override;
void OnSessionStateChanged(session_manager::SessionState state) override;
private:
// ScheduledFeature:
const char* GetFeatureName() const override;
std::unique_ptr<DarkLightModeNudgeController> nudge_controller_;
};
} // namespace ash
#endif // ASH_STYLE_DARK_MODE_CONTROLLER_H_
#endif // ASH_STYLE_DARK_MODE_CONTROLLER_H_

@ -64,6 +64,9 @@ class ASH_EXPORT ScheduledFeature
ScheduledFeature& operator=(const ScheduledFeature&) = delete;
~ScheduledFeature() override;
PrefService* active_user_pref_service() const {
return active_user_pref_service_;
}
base::OneShotTimer* timer() { return &timer_; }
bool GetEnabled() const;