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