chromium/chrome/browser/ash/growth/show_notification_action_performer.cc

// 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/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 ButtonClickCallback& click_callback,
        const CloseCallback& close_callback)
    : click_callback_(click_callback), close_callback_(close_callback) {}
HandleNotificationClickAndCloseDelegate::
    ~HandleNotificationClickAndCloseDelegate() {}

void HandleNotificationClickAndCloseDelegate::Click(
    const std::optional<int>& button_index,
    const std::optional<std::u16string>& reply) {
  if (click_callback_.is_null()) {
    return;
  }
  click_callback_.Run(button_index);
}

void HandleNotificationClickAndCloseDelegate::Close(bool by_user) {
  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>(
              base::BindRepeating(
                  &ShowNotificationActionPerformer::HandleNotificationClicked,
                  weak_ptr_factory_.GetWeakPtr(), params, 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 base::Value::Dict* params,
    const std::string& notification_id,
    int campaign_id,
    std::optional<int> group_id,
    bool should_log_cros_events,
    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.";
  }

  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);
}