Logic for showing contextual nudges.
Bug: 1034168
Change-Id: I8193b727e1635b0f5d368c74157f1ca0f1dd8197
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2025887
Commit-Queue: Yulun Wu <[email protected]>
Reviewed-by: Ahmed Fakhry <[email protected]>
Reviewed-by: Manu Cornet <[email protected]>
Reviewed-by: Xiaoqian Dai <[email protected]>
Reviewed-by: Toni Baržić <[email protected]>
Cr-Commit-Position: refs/heads/master@{#740385}
diff --git a/ash/BUILD.gn b/ash/BUILD.gn
index e2486695..7ad9534 100644
--- a/ash/BUILD.gn
+++ b/ash/BUILD.gn
@@ -581,6 +581,8 @@
"shelf/assistant_overlay.h",
"shelf/back_button.cc",
"shelf/back_button.h",
+ "shelf/contextual_tooltip.cc",
+ "shelf/contextual_tooltip.h",
"shelf/drag_handle.cc",
"shelf/drag_handle.h",
"shelf/home_button.cc",
@@ -1417,6 +1419,7 @@
"//base",
"//base:i18n",
"//base/third_party/dynamic_annotations",
+ "//base/util/values:values_util",
"//build:branding_buildflags",
"//cc",
"//cc/debug",
@@ -1836,6 +1839,7 @@
"screen_util_unittest.cc",
"session/session_controller_impl_unittest.cc",
"shelf/back_button_unittest.cc",
+ "shelf/contextual_tooltip_unittest.cc",
"shelf/home_button_unittest.cc",
"shelf/hotseat_widget_unittest.cc",
"shelf/login_shelf_view_unittest.cc",
@@ -2061,6 +2065,7 @@
"//ash/system/message_center/arc:test_support",
"//base",
"//base/test:test_support",
+ "//base/util/values:values_util",
"//build:branding_buildflags",
"//chromeos:test_support",
"//chromeos/strings:strings_grit",
diff --git a/ash/ash_prefs.cc b/ash/ash_prefs.cc
index ac007e60..e518ecc 100644
--- a/ash/ash_prefs.cc
+++ b/ash/ash_prefs.cc
@@ -14,6 +14,7 @@
#include "ash/magnifier/docked_magnifier_controller_impl.h"
#include "ash/media/media_controller_impl.h"
#include "ash/public/cpp/ash_pref_names.h"
+#include "ash/shelf/contextual_tooltip.h"
#include "ash/shelf/shelf_controller.h"
#include "ash/system/bluetooth/bluetooth_power_controller.h"
#include "ash/system/caps_lock_notification_controller.h"
@@ -40,6 +41,7 @@
AssistantController::RegisterProfilePrefs(registry);
BluetoothPowerController::RegisterProfilePrefs(registry);
CapsLockNotificationController::RegisterProfilePrefs(registry, for_test);
+ contextual_tooltip::RegisterProfilePrefs(registry);
DockedMagnifierControllerImpl::RegisterProfilePrefs(registry);
LoginScreenController::RegisterProfilePrefs(registry, for_test);
LogoutButtonTray::RegisterProfilePrefs(registry);
diff --git a/ash/public/cpp/ash_features.cc b/ash/public/cpp/ash_features.cc
index 3bc25fb..dfc2a42 100644
--- a/ash/public/cpp/ash_features.cc
+++ b/ash/public/cpp/ash_features.cc
@@ -18,6 +18,9 @@
const base::Feature kAutoNightLight{"AutoNightLight",
base::FEATURE_DISABLED_BY_DEFAULT};
+const base::Feature kContextualNudges{"ContextualNudges",
+ base::FEATURE_DISABLED_BY_DEFAULT};
+
const base::Feature kDisplayChangeModal{"DisplayChangeModal",
base::FEATURE_ENABLED_BY_DEFAULT};
@@ -218,6 +221,10 @@
return base::FeatureList::IsEnabled(kDisplayChangeModal);
}
+bool AreContextualNudgesEnabled() {
+ return base::FeatureList::IsEnabled(kContextualNudges);
+}
+
namespace {
// The boolean flag indicating if "WebUITabStrip" feature is enabled in Chrome.
diff --git a/ash/public/cpp/ash_features.h b/ash/public/cpp/ash_features.h
index 6e6171d3..b5bc4051 100644
--- a/ash/public/cpp/ash_features.h
+++ b/ash/public/cpp/ash_features.h
@@ -21,6 +21,9 @@
// certain devices.
ASH_PUBLIC_EXPORT extern const base::Feature kAutoNightLight;
+// Enables contextual nudges for gesture education.
+ASH_PUBLIC_EXPORT extern const base::Feature kContextualNudges;
+
// Enables a modal dialog when resolution or refresh rate change.
ASH_PUBLIC_EXPORT extern const base::Feature kDisplayChangeModal;
@@ -177,6 +180,8 @@
ASH_PUBLIC_EXPORT bool IsDisplayChangeModalEnabled();
+ASH_PUBLIC_EXPORT bool AreContextualNudgesEnabled();
+
// These two functions are supposed to be temporary functions to set or get
// whether "WebUITabStrip" feature is enabled from Chrome.
ASH_PUBLIC_EXPORT void SetWebUITabStripEnabled(bool enabled);
diff --git a/ash/public/cpp/ash_pref_names.cc b/ash/public/cpp/ash_pref_names.cc
index 5351894..a1064265 100644
--- a/ash/public/cpp/ash_pref_names.cc
+++ b/ash/public/cpp/ash_pref_names.cc
@@ -123,6 +123,10 @@
// regardless of the state of a11y features.
const char kShouldAlwaysShowAccessibilityMenu[] = "settings.a11y.enable_menu";
+// A dictionary storing the number of times and most recent time all contextual
+// tooltips have been shown.
+const char kContextualTooltips[] = "settings.contextual_tooltip.shown_info";
+
// A boolean pref storing the enabled status of the Docked Magnifier feature.
const char kDockedMagnifierEnabled[] = "ash.docked_magnifier.enabled";
// A double pref storing the scale value of the Docked Magnifier feature by
diff --git a/ash/public/cpp/ash_pref_names.h b/ash/public/cpp/ash_pref_names.h
index 47b48790..9f6405c 100644
--- a/ash/public/cpp/ash_pref_names.h
+++ b/ash/public/cpp/ash_pref_names.h
@@ -48,6 +48,8 @@
ASH_PUBLIC_EXPORT extern const char kAccessibilityDictationEnabled[];
ASH_PUBLIC_EXPORT extern const char kShouldAlwaysShowAccessibilityMenu[];
+ASH_PUBLIC_EXPORT extern const char kContextualTooltips[];
+
ASH_PUBLIC_EXPORT extern const char kDockedMagnifierEnabled[];
ASH_PUBLIC_EXPORT extern const char kDockedMagnifierScale[];
ASH_PUBLIC_EXPORT extern const char
diff --git a/ash/shelf/contextual_tooltip.cc b/ash/shelf/contextual_tooltip.cc
new file mode 100644
index 0000000..fd8ee9d
--- /dev/null
+++ b/ash/shelf/contextual_tooltip.cc
@@ -0,0 +1,109 @@
+// Copyright 2020 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/shelf/contextual_tooltip.h"
+
+#include "ash/public/cpp/ash_features.h"
+#include "ash/public/cpp/ash_pref_names.h"
+#include "base/strings/strcat.h"
+#include "base/strings/string_util.h"
+#include "base/util/values/values_util.h"
+#include "components/prefs/scoped_user_pref_update.h"
+
+namespace ash {
+
+namespace contextual_tooltip {
+
+namespace {
+
+// Keys for tooltip sub-preferences for shown count and last time shown.
+constexpr char kShownCount[] = "shown_count";
+constexpr char kLastTimeShown[] = "last_time_shown";
+
+base::Clock* g_clock_override = nullptr;
+
+base::Time GetTime() {
+ if (g_clock_override)
+ return g_clock_override->Now();
+ return base::Time::Now();
+}
+
+std::string TooltipTypeToString(TooltipType type) {
+ switch (type) {
+ case TooltipType::kDragHandle:
+ return "drag_handle";
+ }
+ return "invalid";
+}
+
+// Creates the path to the dictionary value from the contextual tooltip type and
+// the sub-preference.
+std::string GetPath(TooltipType type, const std::string& sub_pref) {
+ return base::JoinString({TooltipTypeToString(type), sub_pref}, ".");
+}
+
+int GetShownCount(PrefService* prefs, TooltipType type) {
+ base::Optional<int> shown_count =
+ prefs->GetDictionary(prefs::kContextualTooltips)
+ ->FindIntPath(GetPath(type, kShownCount));
+ return shown_count.value_or(0);
+}
+
+base::Time GetLastShownTime(PrefService* prefs, TooltipType type) {
+ const base::Value* last_shown_time =
+ prefs->GetDictionary(prefs::kContextualTooltips)
+ ->FindPath(GetPath(type, kLastTimeShown));
+ if (!last_shown_time)
+ return base::Time();
+ return *util::ValueToTime(last_shown_time);
+}
+
+} // namespace
+
+void RegisterProfilePrefs(PrefRegistrySimple* registry) {
+ if (features::AreContextualNudgesEnabled())
+ registry->RegisterDictionaryPref(prefs::kContextualTooltips);
+}
+
+bool ShouldShowNudge(PrefService* prefs, TooltipType type) {
+ if (!features::AreContextualNudgesEnabled())
+ return false;
+
+ const int shown_count = GetShownCount(prefs, type);
+ if (shown_count >= kNotificationLimit)
+ return false;
+ if (shown_count == 0)
+ return true;
+ const base::Time last_shown_time = GetLastShownTime(prefs, type);
+ return (GetTime() - last_shown_time) >= kMinInterval;
+}
+
+base::TimeDelta GetNudgeTimeout(PrefService* prefs, TooltipType type) {
+ const int shown_count = GetShownCount(prefs, type);
+ if (shown_count == 0)
+ return base::TimeDelta();
+ DCHECK(ShouldShowNudge(prefs, type));
+ return kNudgeShowDuration;
+}
+
+void HandleNudgeShown(PrefService* prefs, TooltipType type) {
+ const int shown_count = GetShownCount(prefs, type);
+ DictionaryPrefUpdate update(prefs, prefs::kContextualTooltips);
+ update->SetIntPath(GetPath(type, kShownCount), shown_count + 1);
+ update->SetPath(GetPath(type, kLastTimeShown), util::TimeToValue(GetTime()));
+}
+
+void OverrideClockForTesting(base::Clock* test_clock) {
+ DCHECK(!g_clock_override);
+ g_clock_override = test_clock;
+}
+
+void ClearClockOverrideForTesting() {
+ DCHECK(g_clock_override);
+ g_clock_override = nullptr;
+}
+
+} // namespace contextual_tooltip
+
+} // namespace ash
diff --git a/ash/shelf/contextual_tooltip.h b/ash/shelf/contextual_tooltip.h
new file mode 100644
index 0000000..3fe96cc4f
--- /dev/null
+++ b/ash/shelf/contextual_tooltip.h
@@ -0,0 +1,55 @@
+// Copyright 2020 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_SHELF_CONTEXTUAL_TOOLTIP_H_
+#define ASH_SHELF_CONTEXTUAL_TOOLTIP_H_
+
+#include "ash/ash_export.h"
+#include "base/time/clock.h"
+#include "components/prefs/pref_registry_simple.h"
+#include "components/prefs/pref_service.h"
+
+namespace ash {
+
+namespace contextual_tooltip {
+
+// Enumeration of all contextual tooltips.
+enum class TooltipType {
+ kDragHandle,
+};
+
+// Maximum number of times a user can be shown a contextual nudge.
+constexpr int kNotificationLimit = 3;
+
+// Minimum time between showing contextual nudges to the user.
+constexpr base::TimeDelta kMinInterval = base::TimeDelta::FromDays(1);
+
+// The amount of time a nudge is shown.
+constexpr base::TimeDelta kNudgeShowDuration = base::TimeDelta::FromSeconds(5);
+
+// Registers profile prefs.
+ASH_EXPORT void RegisterProfilePrefs(PrefRegistrySimple* registry);
+
+// Returns true if the contextual tooltip of |type| should be shown for the user
+// with the given |prefs|.
+ASH_EXPORT bool ShouldShowNudge(PrefService* prefs, TooltipType type);
+
+// Checks whether the tooltip should be hidden after a timeout. Returns the
+// timeout if it should, returns base::TimeDelta() if not.
+ASH_EXPORT base::TimeDelta GetNudgeTimeout(PrefService* prefs,
+ TooltipType type);
+
+// Increments the counter tracking the number of times the tooltip has been
+// shown. Updates the last shown time for the tooltip.
+ASH_EXPORT void HandleNudgeShown(PrefService* prefs, TooltipType type);
+
+ASH_EXPORT void OverrideClockForTesting(base::Clock* test_clock);
+
+ASH_EXPORT void ClearClockOverrideForTesting();
+
+} // namespace contextual_tooltip
+
+} // namespace ash
+
+#endif // ASH_SHELF_CONTEXTUAL_TOOLTIP_H_
diff --git a/ash/shelf/contextual_tooltip_unittest.cc b/ash/shelf/contextual_tooltip_unittest.cc
new file mode 100644
index 0000000..deda7d5
--- /dev/null
+++ b/ash/shelf/contextual_tooltip_unittest.cc
@@ -0,0 +1,119 @@
+// Copyright 2020 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/shelf/contextual_tooltip.h"
+
+#include "ash/public/cpp/ash_features.h"
+#include "ash/public/cpp/ash_pref_names.h"
+#include "ash/session/session_controller_impl.h"
+#include "ash/shell.h"
+#include "ash/test/ash_test_base.h"
+#include "base/strings/string_util.h"
+#include "base/test/scoped_feature_list.h"
+#include "base/test/simple_test_clock.h"
+#include "base/util/values/values_util.h"
+#include "base/values.h"
+#include "components/prefs/scoped_user_pref_update.h"
+
+namespace ash {
+
+namespace contextual_tooltip {
+
+class ContextualTooltipTest : public AshTestBase,
+ public testing::WithParamInterface<bool> {
+ public:
+ ContextualTooltipTest() {
+ if (GetParam()) {
+ scoped_feature_list_.InitAndEnableFeature(
+ ash::features::kContextualNudges);
+
+ } else {
+ scoped_feature_list_.InitAndDisableFeature(
+ ash::features::kContextualNudges);
+ }
+ }
+ ~ContextualTooltipTest() override = default;
+
+ base::SimpleTestClock* clock() { return &test_clock_; }
+
+ // AshTestBase:
+ void SetUp() override {
+ AshTestBase::SetUp();
+ contextual_tooltip::OverrideClockForTesting(&test_clock_);
+ }
+ void TearDown() override {
+ contextual_tooltip::ClearClockOverrideForTesting();
+ AshTestBase::TearDown();
+ }
+
+ PrefService* GetPrefService() {
+ return Shell::Get()->session_controller()->GetLastActiveUserPrefService();
+ }
+
+ private:
+ base::test::ScopedFeatureList scoped_feature_list_;
+ base::SimpleTestClock test_clock_;
+};
+
+using ContextualTooltipDisabledTest = ContextualTooltipTest;
+
+INSTANTIATE_TEST_SUITE_P(All,
+ ContextualTooltipDisabledTest,
+ testing::Values(false));
+INSTANTIATE_TEST_SUITE_P(All, ContextualTooltipTest, testing::Values(true));
+
+// Checks that nudges are not shown when the feature flag is disabled.
+TEST_P(ContextualTooltipDisabledTest, FeatureFlagDisabled) {
+ EXPECT_FALSE(contextual_tooltip::ShouldShowNudge(GetPrefService(),
+ TooltipType::kDragHandle));
+}
+
+TEST_P(ContextualTooltipTest, ShouldShowPersistentDragHandleNudge) {
+ EXPECT_TRUE(contextual_tooltip::ShouldShowNudge(GetPrefService(),
+ TooltipType::kDragHandle));
+ EXPECT_TRUE(contextual_tooltip::GetNudgeTimeout(GetPrefService(),
+ TooltipType::kDragHandle)
+ .is_zero());
+}
+
+// Checks that drag handle nudge has a timeout if it is not the first time it is
+// being shown.
+TEST_P(ContextualTooltipTest, NonPersistentDragHandleNudgeTimeout) {
+ for (int shown_count = 1;
+ shown_count < contextual_tooltip::kNotificationLimit; shown_count++) {
+ contextual_tooltip::HandleNudgeShown(GetPrefService(),
+ TooltipType::kDragHandle);
+ clock()->Advance(contextual_tooltip::kMinInterval);
+ EXPECT_EQ(contextual_tooltip::GetNudgeTimeout(GetPrefService(),
+ TooltipType::kDragHandle),
+ contextual_tooltip::kNudgeShowDuration);
+ }
+}
+
+// Checks that drag handle nudge should be shown after kMinInterval has passed
+// since the last time it was shown but not before the time interval has passed.
+TEST_P(ContextualTooltipTest, ShouldShowTimedDragHandleNudge) {
+ contextual_tooltip::HandleNudgeShown(GetPrefService(),
+ TooltipType::kDragHandle);
+ for (int shown_count = 1;
+ shown_count < contextual_tooltip::kNotificationLimit; shown_count++) {
+ EXPECT_FALSE(contextual_tooltip::ShouldShowNudge(GetPrefService(),
+ TooltipType::kDragHandle));
+ clock()->Advance(contextual_tooltip::kMinInterval / 2);
+ EXPECT_FALSE(contextual_tooltip::ShouldShowNudge(GetPrefService(),
+ TooltipType::kDragHandle));
+ clock()->Advance(contextual_tooltip::kMinInterval / 2);
+ EXPECT_TRUE(contextual_tooltip::ShouldShowNudge(GetPrefService(),
+ TooltipType::kDragHandle));
+ contextual_tooltip::HandleNudgeShown(GetPrefService(),
+ TooltipType::kDragHandle);
+ }
+ clock()->Advance(contextual_tooltip::kMinInterval);
+ EXPECT_FALSE(contextual_tooltip::ShouldShowNudge(GetPrefService(),
+ TooltipType::kDragHandle));
+}
+
+} // namespace contextual_tooltip
+
+} // namespace ash