Add toast option to dismiss temporarily or permanently

The non-actionable toasts will show a 3-dot menu that has an option
to dismiss it for now or to stop showing the non-actionable toasts.
Additionally a setting is added to control this option.

Screenshot: https://screenshot.googleplex.com/5tQ7V6cFMVBvVf2

Bug: 383135477
Change-Id: I87415454038ec8169831a34bedc7a5fbf18bf4df
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6090300
Commit-Queue: Alison Gale <[email protected]>
Reviewed-by: Lei Zhang <[email protected]>
Reviewed-by: Steven Luong <[email protected]>
Code-Coverage: [email protected] <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1396134}
diff --git a/chrome/browser/prefs/browser_prefs.cc b/chrome/browser/prefs/browser_prefs.cc
index bff0f19..8424b56 100644
--- a/chrome/browser/prefs/browser_prefs.cc
+++ b/chrome/browser/prefs/browser_prefs.cc
@@ -1989,6 +1989,8 @@
   glic::GlicConfiguration::RegisterPrefs(registry);
 #endif
 
+  registry->RegisterIntegerPref(prefs::kToastAlertLevel, 0);
+
   // This is intentionally last.
   RegisterLocalStatePrefsForMigration(registry);
 }
diff --git a/chrome/browser/ui/toasts/BUILD.gn b/chrome/browser/ui/toasts/BUILD.gn
index c73d0e2..82b1596 100644
--- a/chrome/browser/ui/toasts/BUILD.gn
+++ b/chrome/browser/ui/toasts/BUILD.gn
@@ -9,6 +9,7 @@
 source_set("toasts") {
   sources = [
     "toast_controller.h",
+    "toast_dismiss_menu_model.h",
     "toast_features.h",
     "toast_metrics.h",
     "toast_service.h",
@@ -34,6 +35,7 @@
 source_set("impl") {
   sources = [
     "toast_controller.cc",
+    "toast_dismiss_menu_model.cc",
     "toast_features.cc",
     "toast_metrics.cc",
     "toast_service.cc",
@@ -45,6 +47,7 @@
     "//chrome/app:generated_resources",
     "//chrome/app:generated_resources_grit",
     "//chrome/app/vector_icons",
+    "//chrome/browser:browser_process",
     "//chrome/browser:browser_public_dependencies",
     "//chrome/browser/profiles:profile",
     "//chrome/browser/ui:browser_element_identifiers",
diff --git a/chrome/browser/ui/toasts/toast_controller.cc b/chrome/browser/ui/toasts/toast_controller.cc
index dc39e579..23c7c4c 100644
--- a/chrome/browser/ui/toasts/toast_controller.cc
+++ b/chrome/browser/ui/toasts/toast_controller.cc
@@ -4,6 +4,7 @@
 
 #include "chrome/browser/ui/toasts/toast_controller.h"
 
+#include <memory>
 #include <optional>
 #include <string>
 #include <utility>
@@ -17,6 +18,7 @@
 #include "base/location.h"
 #include "base/time/time.h"
 #include "base/timer/timer.h"
+#include "chrome/browser/browser_process.h"
 #include "chrome/browser/ui/browser_window/public/browser_window_features.h"
 #include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
 #include "chrome/browser/ui/exclusive_access/exclusive_access_manager.h"
@@ -25,10 +27,13 @@
 #include "chrome/browser/ui/toasts/api/toast_id.h"
 #include "chrome/browser/ui/toasts/api/toast_registry.h"
 #include "chrome/browser/ui/toasts/api/toast_specification.h"
+#include "chrome/browser/ui/toasts/toast_dismiss_menu_model.h"
 #include "chrome/browser/ui/toasts/toast_features.h"
 #include "chrome/browser/ui/toasts/toast_metrics.h"
 #include "chrome/browser/ui/toasts/toast_view.h"
+#include "chrome/common/pref_names.h"
 #include "components/omnibox/common/omnibox_focus_state.h"
+#include "components/prefs/pref_service.h"
 #include "content/public/browser/web_contents.h"
 #include "ui/base/l10n/l10n_util.h"
 #include "ui/views/bubble/bubble_dialog_delegate_view.h"
@@ -65,8 +70,20 @@
   return GetCurrentToastId().has_value();
 }
 
-bool ToastController::CanShowToast(ToastId id) const {
-  return base::FeatureList::IsEnabled(toast_features::kToastFramework);
+bool ToastController::CanShowToast(ToastId toast_id) const {
+  if (!base::FeatureList::IsEnabled(toast_features::kToastFramework)) {
+    return false;
+  }
+  if (base::FeatureList::IsEnabled(toast_features::kToastRefinements) &&
+      static_cast<toasts::ToastAlertLevel>(
+          g_browser_process->local_state()->GetInteger(
+              prefs::kToastAlertLevel)) ==
+          toasts::ToastAlertLevel::kActionable) {
+    const ToastSpecification* toast_spec =
+        toast_registry_->GetToastSpecification(toast_id);
+    return toast_spec->has_close_button() || toast_spec->has_menu();
+  }
+  return true;
 }
 
 std::optional<ToastId> ToastController::GetCurrentToastId() const {
@@ -75,6 +92,7 @@
 
 bool ToastController::MaybeShowToast(ToastParams params) {
   if (!CanShowToast(params.toast_id)) {
+    RecordToastFailedToShow(params.toast_id);
     return false;
   }
 
@@ -266,6 +284,10 @@
 
   if (params.menu_model) {
     toast_view->AddMenu(std::move(params.menu_model));
+  } else if (base::FeatureList::IsEnabled(toast_features::kToastRefinements) &&
+             !spec->has_close_button()) {
+    toast_view->AddMenu(
+        std::make_unique<ToastDismissMenuModel>(params.toast_id));
   }
 
   toast_view_ = toast_view.get();
diff --git a/chrome/browser/ui/toasts/toast_controller_interactive_ui_test.cc b/chrome/browser/ui/toasts/toast_controller_interactive_ui_test.cc
index 9bbc23b..258708e 100644
--- a/chrome/browser/ui/toasts/toast_controller_interactive_ui_test.cc
+++ b/chrome/browser/ui/toasts/toast_controller_interactive_ui_test.cc
@@ -11,6 +11,7 @@
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/scoped_feature_list.h"
 #include "chrome/app/chrome_command_ids.h"
+#include "chrome/browser/browser_process.h"
 #include "chrome/browser/ui/browser.h"
 #include "chrome/browser/ui/browser_element_identifiers.h"
 #include "chrome/browser/ui/browser_window.h"
@@ -21,11 +22,14 @@
 #include "chrome/browser/ui/omnibox/omnibox_tab_helper.h"
 #include "chrome/browser/ui/toasts/api/toast_id.h"
 #include "chrome/browser/ui/toasts/toast_controller.h"
+#include "chrome/browser/ui/toasts/toast_dismiss_menu_model.h"
 #include "chrome/browser/ui/toasts/toast_features.h"
+#include "chrome/browser/ui/toasts/toast_metrics.h"
 #include "chrome/browser/ui/toasts/toast_view.h"
 #include "chrome/browser/ui/views/frame/app_menu_button.h"
 #include "chrome/browser/ui/views/frame/browser_view.h"
 #include "chrome/browser/ui/views/location_bar/star_view.h"
+#include "chrome/common/pref_names.h"
 #include "chrome/test/base/interactive_test_utils.h"
 #include "chrome/test/base/ui_test_utils.h"
 #include "chrome/test/interaction/interactive_browser_test.h"
@@ -112,8 +116,9 @@
  public:
   void SetUp() override {
     feature_list_.InitWithFeatures(
-        {toast_features::kToastFramework, toast_features::kLinkCopiedToast,
-         toast_features::kImageCopiedToast, toast_features::kReadingListToast,
+        {toast_features::kToastFramework, toast_features::kToastRefinements,
+         toast_features::kLinkCopiedToast, toast_features::kImageCopiedToast,
+         toast_features::kReadingListToast,
          plus_addresses::features::kPlusAddressesEnabled,
          plus_addresses::features::kPlusAddressFullFormFill},
         {});
@@ -375,6 +380,33 @@
 }
 
 IN_PROC_BROWSER_TEST_F(ToastControllerInteractiveTest,
+                       DismissingToastPermanently) {
+  RunTestSequence(
+      ShowToast(ToastParams(ToastId::kLinkCopied)),
+      WaitForShow(toasts::ToastView::kToastViewId),
+      EnsurePresent(toasts::ToastView::kToastMenuButton),
+      PressButton(toasts::ToastView::kToastMenuButton),
+      WaitForShow(ToastDismissMenuModel::kToastDontShowAgainMenuItem),
+      SelectMenuItem(ToastDismissMenuModel::kToastDontShowAgainMenuItem),
+      WaitForHide(toasts::ToastView::kToastViewId),
+      ShowToast(ToastParams(ToastId::kLinkCopied)),
+      EnsureNotPresent(toasts::ToastView::kToastViewId));
+}
+
+IN_PROC_BROWSER_TEST_F(ToastControllerInteractiveTest,
+                       DismissingToastTemporarily) {
+  RunTestSequence(ShowToast(ToastParams(ToastId::kLinkCopied)),
+                  WaitForShow(toasts::ToastView::kToastViewId),
+                  EnsurePresent(toasts::ToastView::kToastMenuButton),
+                  PressButton(toasts::ToastView::kToastMenuButton),
+                  WaitForShow(ToastDismissMenuModel::kToastDismissMenuItem),
+                  SelectMenuItem(ToastDismissMenuModel::kToastDismissMenuItem),
+                  WaitForHide(toasts::ToastView::kToastViewId),
+                  ShowToast(ToastParams(ToastId::kLinkCopied)),
+                  WaitForShow(toasts::ToastView::kToastViewId));
+}
+
+IN_PROC_BROWSER_TEST_F(ToastControllerInteractiveTest,
                        ToastReactToOmniboxFocus) {
   LocationBar* const location_bar = browser()->window()->GetLocationBar();
   ASSERT_TRUE(location_bar);
diff --git a/chrome/browser/ui/toasts/toast_controller_unittest.cc b/chrome/browser/ui/toasts/toast_controller_unittest.cc
index 5c0511ab..19502e74 100644
--- a/chrome/browser/ui/toasts/toast_controller_unittest.cc
+++ b/chrome/browser/ui/toasts/toast_controller_unittest.cc
@@ -6,6 +6,7 @@
 
 #include <memory>
 
+#include "base/test/metrics/histogram_tester.h"
 #include "base/test/scoped_feature_list.h"
 #include "base/test/task_environment.h"
 #include "base/time/time.h"
@@ -13,7 +14,12 @@
 #include "chrome/browser/ui/toasts/api/toast_registry.h"
 #include "chrome/browser/ui/toasts/api/toast_specification.h"
 #include "chrome/browser/ui/toasts/toast_features.h"
+#include "chrome/browser/ui/toasts/toast_metrics.h"
 #include "chrome/browser/ui/toasts/toast_view.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/test/base/scoped_testing_local_state.h"
+#include "chrome/test/base/testing_browser_process.h"
+#include "components/prefs/pref_service.h"
 #include "components/vector_icons/vector_icons.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -170,3 +176,46 @@
   task_environment().FastForwardBy(toast_features::kToastTimeout.Get() / 2);
   EXPECT_TRUE(controller->IsShowingToast());
 }
+
+class ToastControllerWithRefinementsUnitTest : public testing::Test {
+ public:
+  void SetUp() override {
+    feature_list_.InitAndEnableFeatureWithParameters(
+        toast_features::kToastRefinements, {});
+    toast_registry_ = std::make_unique<ToastRegistry>();
+  }
+
+  ToastRegistry* toast_registry() { return toast_registry_.get(); }
+  TestingPrefServiceSimple* local_state() { return local_state_.Get(); }
+  base::HistogramTester* histogram() { return &histogram_; }
+
+ private:
+  base::test::ScopedFeatureList feature_list_;
+  base::test::SingleThreadTaskEnvironment task_environment_{
+      base::test::TaskEnvironment::TimeSource::MOCK_TIME};
+  std::unique_ptr<ToastRegistry> toast_registry_;
+  base::HistogramTester histogram_;
+  ScopedTestingLocalState local_state_{TestingBrowserProcess::GetGlobal()};
+};
+
+TEST_F(ToastControllerWithRefinementsUnitTest, DoesNotShowToastWhenDisabled) {
+  ToastRegistry* const registry = toast_registry();
+  registry->RegisterToast(
+      ToastId::kLinkCopied,
+      ToastSpecification::Builder(vector_icons::kEmailIcon, 0).Build());
+
+  auto controller = std::make_unique<TestToastController>(registry);
+
+  local_state()->SetInteger(
+      prefs::kToastAlertLevel,
+      static_cast<int>(toasts::ToastAlertLevel::kActionable));
+  EXPECT_FALSE(controller->CanShowToast(ToastId::kLinkCopied));
+  EXPECT_FALSE(controller->MaybeShowToast(ToastParams(ToastId::kLinkCopied)));
+
+  histogram()->ExpectBucketCount("Toast.FailedToShow", ToastId::kLinkCopied, 1);
+
+  local_state()->SetInteger(prefs::kToastAlertLevel,
+                            static_cast<int>(toasts::ToastAlertLevel::kAll));
+  EXPECT_TRUE(controller->CanShowToast(ToastId::kLinkCopied));
+  EXPECT_TRUE(controller->MaybeShowToast(ToastParams(ToastId::kLinkCopied)));
+}
diff --git a/chrome/browser/ui/toasts/toast_dismiss_menu_model.cc b/chrome/browser/ui/toasts/toast_dismiss_menu_model.cc
new file mode 100644
index 0000000..ebea2b42
--- /dev/null
+++ b/chrome/browser/ui/toasts/toast_dismiss_menu_model.cc
@@ -0,0 +1,49 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/toasts/toast_dismiss_menu_model.h"
+
+#include "base/notreached.h"
+#include "chrome/browser/browser_process.h"
+#include "chrome/browser/ui/toasts/toast_metrics.h"
+#include "chrome/common/pref_names.h"
+#include "chrome/grit/generated_resources.h"
+#include "components/prefs/pref_service.h"
+#include "ui/base/interaction/element_identifier.h"
+#include "ui/base/l10n/l10n_util.h"
+
+DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(ToastDismissMenuModel,
+                                      kToastDismissMenuItem);
+DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(ToastDismissMenuModel,
+                                      kToastDontShowAgainMenuItem);
+
+ToastDismissMenuModel::ToastDismissMenuModel(ToastId toast_id)
+    : ui::SimpleMenuModel(/*delegate=*/this), toast_id_(toast_id) {
+  AddItem(static_cast<int>(toasts::ToastDismissMenuEntries::kDismiss),
+          l10n_util::GetStringUTF16(IDS_TOAST_MENU_ITEM_DISMISS));
+  SetElementIdentifierAt(0, kToastDismissMenuItem);
+  AddItem(static_cast<int>(toasts::ToastDismissMenuEntries::kDontShowAgain),
+          l10n_util::GetStringUTF16(IDS_TOAST_MENU_ITEM_DONT_SHOW_AGAIN));
+  SetElementIdentifierAt(1, kToastDontShowAgainMenuItem);
+}
+
+ToastDismissMenuModel::~ToastDismissMenuModel() = default;
+
+void ToastDismissMenuModel::ExecuteCommand(int command_id, int event_flags) {
+  const toasts::ToastDismissMenuEntries command =
+      static_cast<toasts::ToastDismissMenuEntries>(command_id);
+  RecordToastDismissMenuClicked(toast_id_, command);
+
+  switch (command) {
+    case toasts::ToastDismissMenuEntries::kDismiss:
+      // Toast will dismiss automatically when a menu item is clicked.
+      return;
+    case toasts::ToastDismissMenuEntries::kDontShowAgain:
+      g_browser_process->local_state()->SetInteger(
+          prefs::kToastAlertLevel,
+          static_cast<int>(toasts::ToastAlertLevel::kActionable));
+      return;
+  }
+  NOTREACHED();
+}
diff --git a/chrome/browser/ui/toasts/toast_dismiss_menu_model.h b/chrome/browser/ui/toasts/toast_dismiss_menu_model.h
new file mode 100644
index 0000000..b8fe585
--- /dev/null
+++ b/chrome/browser/ui/toasts/toast_dismiss_menu_model.h
@@ -0,0 +1,28 @@
+// Copyright 2024 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_TOASTS_TOAST_DISMISS_MENU_MODEL_H_
+#define CHROME_BROWSER_UI_TOASTS_TOAST_DISMISS_MENU_MODEL_H_
+
+#include "chrome/browser/ui/toasts/api/toast_id.h"
+#include "ui/base/interaction/element_identifier.h"
+#include "ui/menus/simple_menu_model.h"
+
+class ToastDismissMenuModel : public ui::SimpleMenuModel,
+                              public ui::SimpleMenuModel::Delegate {
+ public:
+  DECLARE_CLASS_ELEMENT_IDENTIFIER_VALUE(kToastDismissMenuItem);
+  DECLARE_CLASS_ELEMENT_IDENTIFIER_VALUE(kToastDontShowAgainMenuItem);
+
+  explicit ToastDismissMenuModel(ToastId toast_id);
+  ~ToastDismissMenuModel() override;
+
+  // ui::SimpleMenuModel::Delegate:
+  void ExecuteCommand(int command_id, int event_flags) override;
+
+ private:
+  const ToastId toast_id_;
+};
+
+#endif  // CHROME_BROWSER_UI_TOASTS_TOAST_DISMISS_MENU_MODEL_H_
diff --git a/chrome/browser/ui/toasts/toast_features.cc b/chrome/browser/ui/toasts/toast_features.cc
index bff63b4c..acb5e97 100644
--- a/chrome/browser/ui/toasts/toast_features.cc
+++ b/chrome/browser/ui/toasts/toast_features.cc
@@ -16,6 +16,12 @@
              "ToastFramework",
              base::FEATURE_ENABLED_BY_DEFAULT);
 
+// Enables refinements of the toast framework that allow for controlling the
+// visibility of non-actionable toasts.
+BASE_FEATURE(kToastRefinements,
+             "ToastRefinements",
+             base::FEATURE_DISABLED_BY_DEFAULT);
+
 const base::FeatureParam<bool> kToastDemoMode{&kToastFramework,
                                               "toast_demo_mode", false};
 
diff --git a/chrome/browser/ui/toasts/toast_features.h b/chrome/browser/ui/toasts/toast_features.h
index 628d382..5902d1b 100644
--- a/chrome/browser/ui/toasts/toast_features.h
+++ b/chrome/browser/ui/toasts/toast_features.h
@@ -14,6 +14,8 @@
 // Base feature
 BASE_DECLARE_FEATURE(kToastFramework);
 
+BASE_DECLARE_FEATURE(kToastRefinements);
+
 // Enables all toast features queried through `toast_features::IsEnabled` which
 // is used for demo mode.
 extern const base::FeatureParam<bool> kToastDemoMode;
diff --git a/chrome/browser/ui/toasts/toast_metrics.cc b/chrome/browser/ui/toasts/toast_metrics.cc
index 88fe034..af5b23efd 100644
--- a/chrome/browser/ui/toasts/toast_metrics.cc
+++ b/chrome/browser/ui/toasts/toast_metrics.cc
@@ -14,6 +14,10 @@
   base::UmaHistogramEnumeration("Toast.TriggeredToShow", toast_id);
 }
 
+void RecordToastFailedToShow(ToastId toast_id) {
+  base::UmaHistogramEnumeration("Toast.FailedToShow", toast_id);
+}
+
 void RecordToastActionButtonClicked(ToastId toast_id) {
   base::RecordComputedAction(
       base::StrCat({"Toast.ActionButtonClicked.", GetToastName(toast_id)}));
@@ -30,3 +34,10 @@
       base::StrCat({"Toast.", GetToastName(toast_id), ".Dismissed"}),
       close_reason);
 }
+
+void RecordToastDismissMenuClicked(ToastId toast_id,
+                                   toasts::ToastDismissMenuEntries command_id) {
+  base::UmaHistogramEnumeration(
+      base::StrCat({"Toast.", GetToastName(toast_id), ".DismissMenuClicked"}),
+      command_id);
+}
diff --git a/chrome/browser/ui/toasts/toast_metrics.h b/chrome/browser/ui/toasts/toast_metrics.h
index a2a151a2..b3bef94 100644
--- a/chrome/browser/ui/toasts/toast_metrics.h
+++ b/chrome/browser/ui/toasts/toast_metrics.h
@@ -9,10 +9,30 @@
 
 namespace toasts {
 enum class ToastCloseReason;
-}
+
+// These values are persisted to logs. Entries should not be renumbered and
+// numeric values should never be reused.
+enum class ToastAlertLevel {
+  kAll = 0,
+  kActionable = 1,
+  kMaxValue = kActionable
+};
+
+// LINT.IfChange(ToastDismissMenuEntries)
+// These values are persisted to logs. Entries should not be renumbered and
+// numeric values should never be reused.
+enum class ToastDismissMenuEntries {
+  kDismiss = 0,
+  kDontShowAgain = 1,
+  kMaxValue = kDontShowAgain
+};
+// LINT.ThenChange(/tools/metrics/histograms/metadata/toasts/enums.xml:ToastDismissMenuEntries)
+}  // namespace toasts
 
 void RecordToastTriggeredToShow(ToastId toast_id);
 
+void RecordToastFailedToShow(ToastId toast_id);
+
 void RecordToastActionButtonClicked(ToastId toast_id);
 
 void RecordToastCloseButtonClicked(ToastId toast_id);
@@ -20,4 +40,7 @@
 void RecordToastDismissReason(ToastId toast_id,
                               toasts::ToastCloseReason close_reason);
 
+void RecordToastDismissMenuClicked(ToastId toast_id,
+                                   toasts::ToastDismissMenuEntries command_id);
+
 #endif  // CHROME_BROWSER_UI_TOASTS_TOAST_METRICS_H_