| // 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/ash/growth/show_notification_action_performer.h" |
| |
| #include <memory> |
| #include <string> |
| #include <vector> |
| |
| #include "ash/constants/notifier_catalogs.h" |
| #include "ash/public/cpp/notification_utils.h" |
| #include "ash/system/notification_center/message_view_factory.h" |
| #include "base/functional/bind.h" |
| #include "base/logging.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/values.h" |
| #include "chrome/browser/ash/growth/metrics.h" |
| #include "chromeos/ash/components/growth/action_performer.h" |
| #include "chromeos/ash/components/growth/campaigns_logger.h" |
| #include "chromeos/ash/components/growth/campaigns_manager.h" |
| #include "chromeos/ash/components/growth/campaigns_model.h" |
| #include "chromeos/ash/components/growth/growth_metrics.h" |
| #include "chromeos/ash/grit/ash_resources.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/gfx/image/image.h" |
| #include "ui/gfx/vector_icon_utils.h" |
| #include "ui/message_center/message_center.h" |
| #include "ui/message_center/public/cpp/notification.h" |
| #include "ui/message_center/public/cpp/notification_delegate.h" |
| #include "url/gurl.h" |
| |
| namespace { |
| |
| constexpr char kTitlePath[] = "title"; |
| constexpr char kMessagePath[] = "message"; |
| constexpr char kIconPath[] = "sourceIcon"; |
| constexpr char kButtonsPath[] = "buttons"; |
| constexpr char kLabelPath[] = "label"; |
| constexpr char kMarkDismissedPath[] = "shouldMarkDismissed"; |
| constexpr char kActionPath[] = "action"; |
| constexpr char kMarkDismissedOnClosePath[] = "shouldMarkDismissOnClose"; |
| constexpr char kLogCrOSEventsPath[] = "shouldLogCrOSEvents"; |
| constexpr char kImagePath[] = "image"; |
| constexpr char kNotificationIdTemplate[] = "growth_campaign_%d"; |
| |
| struct ShowNotificationParams { |
| std::string title; |
| std::string message; |
| bool should_mark_dismissed_on_close = false; |
| bool should_log_cros_events = false; |
| raw_ptr<const gfx::VectorIcon> icon = nullptr; |
| raw_ptr<const gfx::Image> image = nullptr; |
| |
| std::vector<message_center::ButtonInfo> buttons_info; |
| }; |
| |
| std::unique_ptr<ShowNotificationParams> |
| ParseShowNotificationActionPerformerParams(const base::Value::Dict* params) { |
| if (!params) { |
| CAMPAIGNS_LOG(ERROR) |
| << "Empty parameter to ShowNotificationActionPerformer."; |
| return nullptr; |
| } |
| |
| auto show_notification_params = std::make_unique<ShowNotificationParams>(); |
| |
| const auto* title = params->FindString(kTitlePath); |
| const auto* message = params->FindString(kMessagePath); |
| show_notification_params->title = title ? *title : std::string(); |
| show_notification_params->message = message ? *message : std::string(); |
| |
| show_notification_params->should_mark_dismissed_on_close = |
| params->FindBool(kMarkDismissedOnClosePath).value_or(false); |
| |
| show_notification_params->should_log_cros_events = |
| params->FindBool(kLogCrOSEventsPath).value_or(false); |
| |
| // Set icons if available. |
| const auto* icon_value = params->FindDict(kIconPath); |
| if (!icon_value) { |
| // TODO: b/331633771 - Consider adding default icon for notification. |
| growth::RecordCampaignsManagerError( |
| growth::CampaignsManagerError::kNotificationPayloadMissingIcon); |
| CAMPAIGNS_LOG(ERROR) << "icon is required for notification."; |
| return nullptr; |
| } |
| |
| const auto* icon = growth::VectorIcon(icon_value).GetVectorIcon(); |
| if (!icon) { |
| growth::RecordCampaignsManagerError( |
| growth::CampaignsManagerError::kNotificationPayloadInvalidIcon); |
| return nullptr; |
| } |
| show_notification_params->icon = icon; |
| |
| const auto* image_dict = params->FindDict(kImagePath); |
| if (image_dict) { |
| // TODO: b/341368196 - consider skip showing the notification if the image |
| // type is not recognized. The payload is invalid in this case. |
| show_notification_params->image = growth::Image(image_dict).GetImage(); |
| } |
| |
| // Set buttons info. |
| const auto* buttons = params->FindList(kButtonsPath); |
| if (buttons) { |
| for (auto button_it = buttons->begin(); button_it != buttons->end(); |
| button_it++) { |
| if (!button_it->is_dict()) { |
| growth::RecordCampaignsManagerError( |
| growth::CampaignsManagerError::kNotificationPayloadInvalidButton); |
| continue; |
| } |
| |
| auto* const label = button_it->GetDict().FindString(kLabelPath); |
| if (!label) { |
| growth::RecordCampaignsManagerError( |
| growth::CampaignsManagerError:: |
| kNotificationPayloadMissingButtonLabel); |
| continue; |
| } |
| |
| show_notification_params->buttons_info.emplace_back( |
| base::UTF8ToUTF16(*label)); |
| } |
| } |
| |
| return show_notification_params; |
| } |
| |
| std::string GetNotificationId(int campaign_id) { |
| return base::StringPrintf(kNotificationIdTemplate, campaign_id); |
| } |
| |
| } // namespace |
| |
| HandleNotificationClickAndCloseDelegate:: |
| HandleNotificationClickAndCloseDelegate( |
| const base::Value::Dict* params, |
| const ButtonClickCallback& click_callback, |
| const CloseCallback& close_callback) |
| : // Copy ctor and assignment of Dict are not allowed. Need to use Clone() |
| // explicitly. |
| // `params` are currently owned by CampaignManager. When campaigns are |
| // reloaded at session start (e.g: lock and unlock), the memory will |
| // become invalid, so store the value in |
| // HandleNotificationClickAndCloseDelegate. |
| // HandleNotificationClickAndCloseDelegate is one instance per |
| // notification. |
| params_(params->Clone()), |
| click_callback_(click_callback), |
| close_callback_(close_callback) {} |
| HandleNotificationClickAndCloseDelegate:: |
| ~HandleNotificationClickAndCloseDelegate() = default; |
| |
| void HandleNotificationClickAndCloseDelegate::Click( |
| const std::optional<int>& button_index, |
| const std::optional<std::u16string>& reply) { |
| if (button_index) { |
| button_clicked_ = true; |
| } |
| |
| if (click_callback_.is_null()) { |
| return; |
| } |
| |
| // Need to make another clone here because the Dict value is passed to |
| // multiple functions. If pass by reference, current test will fail due to |
| // dangling pointer. |
| click_callback_.Run(params_.Clone(), button_index); |
| } |
| |
| void HandleNotificationClickAndCloseDelegate::Close(bool by_user) { |
| // Click any button in the notification will also trigger `Close()`. |
| // Return here because we only want to log the close metric with explicit |
| // close actions, such as clicking the close button. |
| if (button_clicked_) { |
| return; |
| } |
| |
| if (close_callback_.is_null()) { |
| return; |
| } |
| close_callback_.Run(by_user); |
| } |
| |
| DEFINE_CLASS_ELEMENT_IDENTIFIER_VALUE(ShowNotificationActionPerformer, |
| kBubbleIdForTesting); |
| |
| ShowNotificationActionPerformer::ShowNotificationActionPerformer() = default; |
| ShowNotificationActionPerformer::~ShowNotificationActionPerformer() = default; |
| |
| void ShowNotificationActionPerformer::Run( |
| int campaign_id, |
| std::optional<int> group_id, |
| const base::Value::Dict* params, |
| growth::ActionPerformer::Callback callback) { |
| // Cache the campaign ID |
| current_campaign_id_ = campaign_id; |
| auto show_notification_params = |
| ParseShowNotificationActionPerformerParams(params); |
| if (!show_notification_params) { |
| growth::RecordCampaignsManagerError( |
| growth::CampaignsManagerError::kInvalidNotificationPayload); |
| std::move(callback).Run(growth::ActionResult::kFailure, |
| growth::ActionResultReason::kParsingActionFailed); |
| return; |
| } |
| |
| message_center::RichNotificationData optional_fields; |
| optional_fields.buttons = std::move(show_notification_params->buttons_info); |
| if (show_notification_params->image) { |
| optional_fields.image = *show_notification_params->image; |
| } |
| |
| auto id = GetNotificationId(campaign_id); |
| std::unique_ptr<message_center::Notification> notification = |
| ash::CreateSystemNotificationPtr( |
| message_center::NOTIFICATION_TYPE_SIMPLE, id, |
| base::UTF8ToUTF16(show_notification_params->title), |
| base::UTF8ToUTF16(show_notification_params->message), |
| /*display_source=*/std::u16string(), |
| /*origin_url=*/GURL(), |
| message_center::NotifierId( |
| message_center::NotifierType::SYSTEM_COMPONENT, id, |
| ash::NotificationCatalogName::kGrowthFramework), |
| optional_fields, |
| base::MakeRefCounted<HandleNotificationClickAndCloseDelegate>( |
| params, |
| base::BindRepeating( |
| &ShowNotificationActionPerformer::HandleNotificationClicked, |
| weak_ptr_factory_.GetWeakPtr(), id, campaign_id, group_id, |
| show_notification_params->should_log_cros_events), |
| base::BindRepeating( |
| &ShowNotificationActionPerformer::HandleNotificationClose, |
| weak_ptr_factory_.GetWeakPtr(), campaign_id, group_id, |
| show_notification_params->should_mark_dismissed_on_close, |
| show_notification_params->should_log_cros_events)), |
| *show_notification_params->icon, |
| message_center::SystemNotificationWarningLevel::NORMAL); |
| notification->set_host_view_element_id(kBubbleIdForTesting); |
| |
| auto* message_center = message_center::MessageCenter::Get(); |
| CHECK(message_center); |
| |
| message_center->RemoveNotification(notification->id(), |
| /*by_user=*/false); |
| message_center->AddNotification(std::move(notification)); |
| |
| NotifyReadyToLogImpression(campaign_id, group_id, |
| show_notification_params->should_log_cros_events); |
| std::move(callback).Run(growth::ActionResult::kSuccess, |
| /*action_result_reason=*/std::nullopt); |
| } |
| |
| growth::ActionType ShowNotificationActionPerformer::ActionType() const { |
| return growth::ActionType::kShowNotification; |
| } |
| |
| void ShowNotificationActionPerformer::HandleNotificationClose( |
| int campaign_id, |
| std::optional<int> group_id, |
| bool should_mark_dismissed, |
| bool should_log_cros_events, |
| bool by_user) { |
| if (!by_user) { |
| return; |
| } |
| |
| // Dismiss and marked the notification dismissed as it is by user action. |
| NotifyButtonPressed(campaign_id, group_id, CampaignButtonId::kClose, |
| should_mark_dismissed, should_log_cros_events); |
| } |
| |
| void ShowNotificationActionPerformer::HandleNotificationClicked( |
| const std::string& notification_id, |
| int campaign_id, |
| std::optional<int> group_id, |
| bool should_log_cros_events, |
| base::Value::Dict params, |
| std::optional<int> button_index) { |
| if (!button_index) { |
| // Notification message body clicked. |
| return; |
| } |
| |
| auto button_index_value = button_index.value(); |
| auto button_id = CampaignButtonId::kOthers; |
| if (button_index_value == 0) { |
| button_id = CampaignButtonId::kPrimary; |
| } else if (button_index_value == 1) { |
| button_id = CampaignButtonId::kSecondary; |
| } |
| |
| const auto* buttons_value = params.FindList(kButtonsPath); |
| CHECK(buttons_value); |
| |
| const auto& button_value = (*buttons_value)[button_index_value]; |
| if (!button_value.is_dict()) { |
| CAMPAIGNS_LOG(ERROR) << "Invalid button payload."; |
| return; |
| } |
| |
| const auto should_mark_dismissed = |
| button_value.GetDict().FindBool(kMarkDismissedPath).value_or(false); |
| NotifyButtonPressed(campaign_id, group_id, button_id, should_mark_dismissed, |
| should_log_cros_events); |
| |
| const auto* action_value = button_value.GetDict().FindDict(kActionPath); |
| if (!action_value) { |
| growth::RecordCampaignsManagerError( |
| growth::CampaignsManagerError::kNotificationPayloadMissingButtonAction); |
| CAMPAIGNS_LOG(ERROR) << "Missing action."; |
| return; |
| } |
| auto action = growth::Action(action_value); |
| if (action.GetActionType() == growth::ActionType::kDismiss) { |
| message_center::MessageCenter::Get()->RemoveNotification(notification_id, |
| /* by_user=*/true); |
| return; |
| } |
| |
| auto* campaigns_manager = growth::CampaignsManager::Get(); |
| CHECK(campaigns_manager); |
| |
| campaigns_manager->PerformAction(campaign_id, group_id, &action); |
| |
| // Explicitly remove the notification as the notification framework doesn't |
| // automatically close at buttons click. |
| message_center::MessageCenter::Get()->RemoveNotification(notification_id, |
| /* by_user=*/true); |
| } |