// Copyright 2014 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/notifications/platform_notification_service_impl.h"

#include <memory>
#include <optional>
#include <set>
#include <utility>
#include <vector>

#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "base/notimplemented.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/browser_features.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/metrics/ukm_background_recorder_service.h"
#include "chrome/browser/notifications/metrics/notification_metrics_logger.h"
#include "chrome/browser/notifications/metrics/notification_metrics_logger_factory.h"
#include "chrome/browser/notifications/notification_display_service.h"
#include "chrome/browser/notifications/notification_display_service_factory.h"
#include "chrome/browser/permissions/notifications_engagement_service_factory.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_context.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_manager.h"
#include "chrome/browser/ui/safety_hub/abusive_notification_permissions_manager.h"
#include "chrome/browser/ui/safety_hub/disruptive_notification_permissions_manager.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/pref_names.h"
#include "chrome/grit/generated_resources.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/permissions/features.h"
#include "components/permissions/permission_uma_util.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/prefs/pref_change_registrar.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/safe_browsing/buildflags.h"
#include "components/safe_browsing/content/browser/notification_content_detection/notification_content_detection_constants.h"
#include "components/safe_browsing/core/common/safe_browsing_prefs.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/platform_notification_context.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/storage_partition_config.h"
#include "services/metrics/public/cpp/ukm_builders.h"
#include "third_party/blink/public/common/notifications/notification_resources.h"
#include "third_party/blink/public/common/notifications/platform_notification_data.h"
#include "third_party/blink/public/mojom/notifications/notification.mojom.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/message_center/public/cpp/notification.h"
#include "ui/message_center/public/cpp/notification_types.h"
#include "ui/message_center/public/cpp/notifier_id.h"
#include "ui/strings/grit/ui_strings.h"
#include "url/origin.h"

#if !BUILDFLAG(IS_ANDROID)
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"  // nogncheck crbug.com/40147906
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/web_applications/proto/web_app_install_state.pb.h"  // nogncheck
#include "chrome/browser/web_applications/web_app_icon_manager.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#else
#include "chrome/browser/safe_browsing/android/notification_content_detection_manager_android.h"
#endif

#if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
#include "extensions/browser/extension_registry.h"
#include "extensions/common/constants.h"
#endif

#if BUILDFLAG(IS_CHROMEOS)
#include "base/metrics/histogram_functions.h"
#endif  // IS_CHROMEOS

#if BUILDFLAG(SAFE_BROWSING_AVAILABLE)
#include "chrome/browser/safe_browsing/notification_content_detection/notification_content_detection_service_factory.h"
#include "components/safe_browsing/content/browser/notification_content_detection/notification_content_detection_service.h"
#endif

using content::BrowserContext;
using content::BrowserThread;
using content::NotificationDatabaseData;
using message_center::NotifierId;

namespace {

constexpr char
    kNotificationContentDetectionDisplayPersistentNotificationEventHistogram[] =
        "SafeBrowsing.NotificationContentDetection."
        "DisplayPersistentNotificationEvent";

#if BUILDFLAG(IS_CHROMEOS)

constexpr char kNotificationResourceImageMemorySizeHistogram[] =
    "Ash.NotificationResource.ImageMemorySizeInKB";

#endif  // IS_CHROMEOS

// Whether a web notification should be displayed when chrome is in full
// screen mode.
static bool ShouldDisplayWebNotificationOnFullScreen(Profile* profile,
                                                     const GURL& origin) {
#if BUILDFLAG(IS_ANDROID)
  NOTIMPLEMENTED();
  return false;
#else
  // Check to see if this notification comes from a webpage that is displaying
  // fullscreen content.
  bool found = false;
  ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
      [profile, &origin,
       &found](BrowserWindowInterface* browser_window_interface) {
        // Only consider the browsers for the profile that created the
        // notification
        const Profile* const browser_profile =
            browser_window_interface->GetProfile();
        if (browser_profile != profile) {
          return true;
        }

        content::WebContents* const active_contents =
            browser_window_interface->GetTabStripModel()
                ->GetActiveWebContents();
        if (!active_contents) {
          return true;
        }

        // Check to see if
        //  (a) the active tab in the browser shares its origin with the
        //      notification.
        //  (b) the browser is fullscreen
        //  (c) the browser has focus.
        if (active_contents->GetURL().DeprecatedGetOriginAsURL() == origin &&
            browser_window_interface->GetFeatures()
                .exclusive_access_manager()
                ->context()
                ->IsFullscreen() &&
            browser_window_interface->GetWindow()->IsActive()) {
          found = true;
        }
        return !found;
      });
  return found;
#endif
}

// Records the total number of deleted notifications after all storage
// partitions are done and called OnDeleted. Uses the ref count to keep track
// of pending callbacks.
class RevokeDeleteCountRecorder
    : public base::RefCounted<RevokeDeleteCountRecorder> {
 public:
  RevokeDeleteCountRecorder() : total_deleted_count_(0) {}
  RevokeDeleteCountRecorder(const RevokeDeleteCountRecorder&) = delete;
  RevokeDeleteCountRecorder& operator=(const RevokeDeleteCountRecorder&) =
      delete;

  void OnDeleted(bool success, size_t deleted_count) {
    total_deleted_count_ += deleted_count;
  }

 private:
  friend class base::RefCounted<RevokeDeleteCountRecorder>;

  ~RevokeDeleteCountRecorder() {
    UMA_HISTOGRAM_COUNTS_100("Notifications.Permissions.RevokeDeleteCount",
                             total_deleted_count_);
  }

  size_t total_deleted_count_;
};

// The type of event when displaying a persistent notification. These values
// are persisted to logs. Entries should not be renumbered and numeric values
// should never be reused.
enum class DisplayPersistentNotificationEvents {
  // The event logged when requesting to display a persistent notification.
  kRequested = 0,
  // The event logged when model checking and displaying the persistent
  // notification have completed.
  kFinished = 1,
  kMaxValue = kFinished,
};

}  // namespace

// static
void PlatformNotificationServiceImpl::RegisterProfilePrefs(
    user_prefs::PrefRegistrySyncable* registry) {
  // The first persistent ID is registered as 10000 rather than 1 to prevent the
  // reuse of persistent notification IDs, which must be unique. Reuse of
  // notification IDs may occur as they were previously stored in a different
  // data store.
  registry->RegisterIntegerPref(prefs::kNotificationNextPersistentId, 10000);

  // Store the next notification trigger time for each profile. If none is set,
  // this will default to base::Time::Max.
  registry->RegisterTimePref(prefs::kNotificationNextTriggerTime,
                             base::Time::Max());
}

PlatformNotificationServiceImpl::PlatformNotificationServiceImpl(
    Profile* profile)
    : profile_(profile),
      trigger_scheduler_(NotificationTriggerScheduler::Create()) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DCHECK(profile_);
  HostContentSettingsMapFactory::GetForProfile(profile_)->AddObserver(this);
}

PlatformNotificationServiceImpl::~PlatformNotificationServiceImpl() = default;

void PlatformNotificationServiceImpl::Shutdown() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  HostContentSettingsMapFactory::GetForProfile(profile_)->RemoveObserver(this);
  // Clear the profile as we're not supposed to use it anymore.
  profile_ = nullptr;
}

void PlatformNotificationServiceImpl::OnContentSettingChanged(
    const ContentSettingsPattern& primary_pattern,
    const ContentSettingsPattern& secondary_pattern,
    ContentSettingsTypeSet content_type_set) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  if (!content_type_set.Contains(ContentSettingsType::NOTIFICATIONS))
    return;

  auto recorder = base::MakeRefCounted<RevokeDeleteCountRecorder>();
  profile_->ForEachLoadedStoragePartition(
      [&](content::StoragePartition* partition) {
        partition->GetPlatformNotificationContext()
            ->DeleteAllNotificationDataForBlockedOrigins(base::BindOnce(
                &RevokeDeleteCountRecorder::OnDeleted, recorder));
      });
}

bool PlatformNotificationServiceImpl::WasClosedProgrammatically(
    const std::string& notification_id) {
  return closed_notifications_.erase(notification_id) != 0;
}

// TODO(awdf): Rename to DisplayNonPersistentNotification (Similar for Close)
void PlatformNotificationServiceImpl::DisplayNotification(
    const std::string& notification_id,
    const GURL& origin,
    const GURL& document_url,
    const blink::PlatformNotificationData& notification_data,
    const blink::NotificationResources& notification_resources) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  // Posted tasks can request notifications to be added, which would cause a
  // crash (see |ScopedKeepAlive|). We just do nothing here, the user would not
  // see the notification anyway, since we are shutting down.
  if (g_browser_process->IsShuttingDown() || !profile_)
    return;

  DCHECK_EQ(0u, notification_data.actions.size());
  DCHECK_EQ(0u, notification_resources.action_icons.size());

  message_center::Notification notification =
      CreateNotificationFromData(origin, notification_id, notification_data,
                                 notification_resources, document_url);
  auto metadata = std::make_unique<NonPersistentNotificationMetadata>();
  metadata->document_url = document_url;

  NotificationDisplayServiceFactory::GetForProfile(profile_)->Display(
      NotificationHandler::Type::WEB_NON_PERSISTENT, notification,
      std::move(metadata));

  permissions::PermissionUmaUtil::RecordPermissionUsage(
      ContentSettingsType::NOTIFICATIONS, profile_, nullptr,
      notification.origin_url());

  auto* service =
      NotificationsEngagementServiceFactory::GetForProfile(profile_);
  // This service might be missing for incognito profiles and in tests.
  if (service) {
    service->RecordNotificationDisplayed(notification.origin_url());
  }

  // Logs metrics for proposed disruptive notification revocation when
  // displaying a non persistent notification. Disruptive are notifications
  // with high notification volume and low site engagement score.
  ukm::SourceId source_id = ukm::UkmRecorder::GetSourceIdForNotificationEvent(
      base::PassKey<PlatformNotificationServiceImpl>(),
      notification.origin_url());
  DisruptiveNotificationPermissionsManager::LogMetrics(
      profile_, notification.origin_url(), source_id);
}

void PlatformNotificationServiceImpl::DisplayPersistentNotification(
    const std::string& notification_id,
    const GURL& service_worker_scope,
    const GURL& origin,
    const blink::PlatformNotificationData& notification_data,
    const blink::NotificationResources& notification_resources) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);

  closed_notifications_.erase(notification_id);

  // Posted tasks can request notifications to be added, which would cause a
  // crash (see |ScopedKeepAlive|). We just do nothing here, the user would not
  // see the notification anyway, since we are shutting down.
  // Note that the IsShuttingDown() check should not be required here, but some
  // tests try to display a notification during shutdown.
  if (g_browser_process->IsShuttingDown() || !profile_)
    return;

  message_center::Notification notification =
      CreateNotificationFromData(origin, notification_id, notification_data,
                                 notification_resources, service_worker_scope);
  auto metadata = std::make_unique<PersistentNotificationMetadata>();
  metadata->service_worker_scope = service_worker_scope;

#if BUILDFLAG(SAFE_BROWSING_AVAILABLE)
  if (safe_browsing::IsSafeBrowsingEnabled(*profile_->GetPrefs()) &&
      !safe_browsing::IsURLAllowlistedByPolicy(origin, *profile_->GetPrefs())) {
    auto* notification_content_service = safe_browsing::
        NotificationContentDetectionServiceFactory::GetForProfile(profile_);
    if (notification_content_service) {
      bool is_show_warnings_for_suspicious_notifications_enabled =
          base::FeatureList::IsEnabled(
              safe_browsing::kShowWarningsForSuspiciousNotifications);
      notification_content_service->MaybeCheckNotificationContentDetectionModel(
          notification_data, origin,
          AreSuspiciousNotificationsAllowlistedByUser(origin),
          is_show_warnings_for_suspicious_notifications_enabled
              ? base::BindOnce(&PlatformNotificationServiceImpl::
                                   HandleOnDeviceModelResponseThenMaybeDisplay,
                               weak_ptr_factory_.GetWeakPtr(), notification,
                               std::move(metadata))
              : base::DoNothing());
      // When this feature is enabled, the
      // `MaybeCheckNotificationContentDetectionModel` method will also include
      // displaying the notification. In this case, the metrics should be logged
      // and the method should return without calling `Display`. Otherwise, the
      // notification should be displayed below.
      if (is_show_warnings_for_suspicious_notifications_enabled) {
        base::UmaHistogramEnumeration(
            kNotificationContentDetectionDisplayPersistentNotificationEventHistogram,
            DisplayPersistentNotificationEvents::kRequested);
        LogPersistentNotificationShownMetrics(notification_data, origin,
                                              notification.origin_url());
        return;
      }
    }
  }
#endif

  NotificationDisplayServiceFactory::GetForProfile(profile_)->Display(
      NotificationHandler::Type::WEB_PERSISTENT, notification,
      std::move(metadata));

  LogPersistentNotificationShownMetrics(notification_data, origin,
                                        notification.origin_url());

  // Logs metrics for proposed disruptive notification revocation when
  // displaying a persistent notification. Disruptive are notifications with
  // high notification volume and low site engagement score.
  ukm::SourceId source_id = ukm::UkmRecorder::GetSourceIdForNotificationEvent(
      base::PassKey<PlatformNotificationServiceImpl>(),
      notification.origin_url());
  DisruptiveNotificationPermissionsManager::LogMetrics(
      profile_, notification.origin_url(), source_id);
}

void PlatformNotificationServiceImpl::CloseNotification(
    const std::string& notification_id) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (g_browser_process->IsShuttingDown() || !profile_)
    return;

  NotificationDisplayServiceFactory::GetForProfile(profile_)->Close(
      NotificationHandler::Type::WEB_NON_PERSISTENT, notification_id);
}

void PlatformNotificationServiceImpl::ClosePersistentNotification(
    const std::string& notification_id) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (g_browser_process->IsShuttingDown() || !profile_)
    return;

  closed_notifications_.insert(notification_id);

  NotificationDisplayServiceFactory::GetForProfile(profile_)->Close(
      NotificationHandler::Type::WEB_PERSISTENT, notification_id);
}

void PlatformNotificationServiceImpl::GetDisplayedNotifications(
    DisplayedNotificationsCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (g_browser_process->IsShuttingDown() || !profile_)
    return;

  // Tests will not have a message center.
  if (profile_->AsTestingProfile()) {
    std::set<std::string> displayed_notifications;
    std::move(callback).Run(std::move(displayed_notifications),
                            false /* supports_synchronization */);
    return;
  }
  NotificationDisplayServiceFactory::GetForProfile(profile_)->GetDisplayed(
      std::move(callback));
}

void PlatformNotificationServiceImpl::GetDisplayedNotificationsForOrigin(
    const GURL& origin,
    DisplayedNotificationsCallback callback) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (g_browser_process->IsShuttingDown() || !profile_) {
    return;
  }

  // Tests will not have a message center.
  if (profile_->AsTestingProfile()) {
    std::set<std::string> displayed_notifications;
    std::move(callback).Run(std::move(displayed_notifications),
                            false /* supports_synchronization */);
    return;
  }
  NotificationDisplayServiceFactory::GetForProfile(profile_)
      ->GetDisplayedForOrigin(origin, std::move(callback));
}

void PlatformNotificationServiceImpl::ScheduleTrigger(base::Time timestamp) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (g_browser_process->IsShuttingDown() || !profile_)
    return;

  PrefService* prefs = profile_->GetPrefs();
  base::Time current_trigger =
      prefs->GetTime(prefs::kNotificationNextTriggerTime);

  if (current_trigger > timestamp)
    prefs->SetTime(prefs::kNotificationNextTriggerTime, timestamp);

}

base::Time PlatformNotificationServiceImpl::ReadNextTriggerTimestamp() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (g_browser_process->IsShuttingDown() || !profile_)
    return base::Time::Max();

  PrefService* prefs = profile_->GetPrefs();
  return prefs->GetTime(prefs::kNotificationNextTriggerTime);
}

int64_t PlatformNotificationServiceImpl::ReadNextPersistentNotificationId() {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (g_browser_process->IsShuttingDown() || !profile_)
    return 0;

  PrefService* prefs = profile_->GetPrefs();

  int64_t current_id = prefs->GetInteger(prefs::kNotificationNextPersistentId);
  int64_t next_id = current_id + 1;

  prefs->SetInteger(prefs::kNotificationNextPersistentId, next_id);
  return next_id;
}

void PlatformNotificationServiceImpl::RecordNotificationUkmEvent(
    const NotificationDatabaseData& data) {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  if (g_browser_process->IsShuttingDown() || !profile_)
    return;

  // Only record the event if a user explicitly interacted with the notification
  // to close it.
  if (data.closed_reason != NotificationDatabaseData::ClosedReason::USER &&
      data.num_clicks == 0 && data.num_action_button_clicks == 0) {
    return;
  }

  ukm::SourceId source_id = ukm::UkmRecorder::GetSourceIdForNotificationEvent(
      base::PassKey<PlatformNotificationServiceImpl>(), data.origin);

  RecordNotificationUkmEventWithSourceId(
      std::move(ukm_recorded_closure_for_testing_), data, source_id);
}

NotificationTriggerScheduler*
PlatformNotificationServiceImpl::GetNotificationTriggerScheduler() {
  return trigger_scheduler_.get();
}

// static
void PlatformNotificationServiceImpl::RecordNotificationUkmEventWithSourceId(
    base::OnceClosure recorded_closure,
    const content::NotificationDatabaseData& data,
    ukm::SourceId source_id) {
  ukm::builders::Notification builder(source_id);

  int64_t time_until_first_click_millis =
      data.time_until_first_click_millis.has_value()
          ? data.time_until_first_click_millis.value().InMilliseconds()
          : -1;

  int64_t time_until_last_click_millis =
      data.time_until_last_click_millis.has_value()
          ? data.time_until_last_click_millis.value().InMilliseconds()
          : -1;

  int64_t time_until_close_millis =
      data.time_until_close_millis.has_value()
          ? data.time_until_close_millis.value().InMilliseconds()
          : -1;

  // TODO(yangsharon):Add did_user_open_settings field and update here.
  builder.SetClosedReason(static_cast<int>(data.closed_reason))
      .SetDidReplaceAnotherNotification(data.replaced_existing_notification)
      .SetHasBadge(!data.notification_data.badge.is_empty())
      .SetHasIcon(!data.notification_data.icon.is_empty())
      .SetHasImage(!data.notification_data.image.is_empty())
      .SetHasRenotify(data.notification_data.renotify)
      .SetHasTag(!data.notification_data.tag.empty())
      .SetIsSilent(data.notification_data.silent)
      .SetNumActions(data.notification_data.actions.size())
      .SetNumActionButtonClicks(data.num_action_button_clicks)
      .SetNumClicks(data.num_clicks)
      .SetRequireInteraction(data.notification_data.require_interaction)
      .SetTimeUntilClose(time_until_close_millis)
      .SetTimeUntilFirstClick(time_until_first_click_millis)
      .SetTimeUntilLastClick(time_until_last_click_millis)
      .Record(ukm::UkmRecorder::Get());

  if (recorded_closure)
    std::move(recorded_closure).Run();
}

message_center::Notification
PlatformNotificationServiceImpl::CreateNotificationFromData(
    const GURL& origin,
    const std::string& notification_id,
    const blink::PlatformNotificationData& notification_data,
    const blink::NotificationResources& notification_resources,
    const GURL& web_app_hint_url) const {
  // Blink always populates action icons to match the actions, even if no icon
  // was fetched, so this indicates a compromised renderer.
  CHECK_EQ(notification_data.actions.size(),
           notification_resources.action_icons.size());

  message_center::RichNotificationData optional_fields;

  optional_fields.settings_button_handler =
      message_center::SettingsButtonHandler::INLINE;

  // TODO(crbug.com/40277066): We can do a better job than basing this
  // purely on `web_app_hint_url`, for example for non-persistent notifications
  // triggered from workers (where `web_app_hint_url` is always blank) but also
  // for persistent notifications triggered from web pages (where the page url
  // might be a better "hint" than the service worker scope).
  std::optional<webapps::AppId> web_app_id = FindWebAppId(web_app_hint_url);

  std::optional<WebAppIconAndTitle> web_app_icon_and_title;
#if BUILDFLAG(IS_CHROMEOS)
  web_app_icon_and_title = FindWebAppIconAndTitle(web_app_hint_url);
  if (web_app_icon_and_title && notification_resources.badge.isNull()) {
    // ChromeOS: Enables web app theme color only if monochrome web app icon
    // has been specified. `badge` Notifications API icons must be masked with
    // the accent color.
    optional_fields.ignore_accent_color_for_small_image = true;
  }
#endif  // BUILDFLAG(IS_CHROMEOS)

  message_center::NotifierId notifier_id(
      origin,
      web_app_icon_and_title ? std::make_optional(web_app_icon_and_title->title)
                             : std::nullopt,
      web_app_id);

  // TODO(peter): Handle different screen densities instead of always using the
  // 1x bitmap - crbug.com/41238973.
  message_center::Notification notification(
      message_center::NOTIFICATION_TYPE_SIMPLE, notification_id,
      notification_data.title, notification_data.body,
      ui::ImageModel::FromImage(gfx::Image::CreateFrom1xBitmap(
          notification_resources.notification_icon)),
      base::UTF8ToUTF16(origin.GetHost()), origin, notifier_id, optional_fields,
      nullptr /* delegate */);

  notification.set_context_message(DisplayNameForContextMessage(origin));
  notification.set_vibration_pattern(notification_data.vibration_pattern);
  notification.set_timestamp(notification_data.timestamp);
  notification.set_renotify(notification_data.renotify);
  notification.set_silent(notification_data.silent);
  if (ShouldDisplayWebNotificationOnFullScreen(profile_, origin)) {
    notification.set_fullscreen_visibility(
        message_center::FullscreenVisibility::OVER_USER);
  }

  if (const SkBitmap& image = notification_resources.image;
      !image.drawsNothing()) {
    notification.set_type(message_center::NOTIFICATION_TYPE_IMAGE);
    notification.SetImage(gfx::Image::CreateFrom1xBitmap(image));
#if BUILDFLAG(IS_CHROMEOS)
    base::UmaHistogramMemoryKB(kNotificationResourceImageMemorySizeHistogram,
                               image.computeByteSize() / 1024);
#endif  // BUILDFLAG(IS_CHROMEOS)
  }

  if (web_app_icon_and_title && !web_app_icon_and_title->icon.isNull())
    notification.SetSmallImage(gfx::Image(web_app_icon_and_title->icon));

  // TODO(peter): Handle different screen densities instead of always using the
  // 1x bitmap - crbug.com/41238973.
  if (const SkBitmap& badge = notification_resources.badge; !badge.isNull()) {
    notification.SetSmallImage(gfx::Image::CreateFrom1xBitmap(badge));
  }

  // Developer supplied action buttons.
  std::vector<message_center::ButtonInfo> buttons;
  for (size_t i = 0; i < notification_data.actions.size(); ++i) {
    const auto& action = notification_data.actions[i];
    message_center::ButtonInfo button(action->title);
    // TODO(peter): Handle different screen densities instead of always using
    // the 1x bitmap - crbug.com/41238973.
    const SkBitmap& action_icon = notification_resources.action_icons[i];
    button.icon = gfx::Image::CreateFrom1xBitmap(action_icon);
    if (action->type == blink::mojom::NotificationActionType::TEXT) {
      button.placeholder = action->placeholder.value_or(
          l10n_util::GetStringUTF16(IDS_NOTIFICATION_REPLY_PLACEHOLDER));
    }
    buttons.push_back(button);
  }
  notification.set_buttons(buttons);

  // On desktop, notifications with require_interaction==true stay on-screen
  // rather than minimizing to the notification center after a timeout.
  // On mobile, this is ignored (notifications are minimized at all times).
  if (notification_data.require_interaction)
    notification.set_never_timeout(true);

  notification.set_scenario(message_center::NotificationScenario::DEFAULT);
  if (base::FeatureList::IsEnabled(features::kIncomingCallNotifications) &&
      notification_data.scenario ==
          blink::mojom::NotificationScenario::INCOMING_CALL) {
    // If the origin is not installed, the notification scenario should be set
    // to DEFAULT.
    if (IsActivelyInstalledWebAppScope(web_app_hint_url)) {
      notification.set_scenario(
          message_center::NotificationScenario::INCOMING_CALL);
    } else {
      notification.set_scenario(message_center::NotificationScenario::DEFAULT);
    }

    // Create the default incoming call dismiss button and set the button types
    // accordingly to fit the incoming call scenario - i.e., developer supplied
    // action buttond are of type ButtonType::ACKNOWLEDGE and the default dimiss
    // button is of type ButtonType::DISMISS
    message_center::ButtonInfo default_dismiss_button(
        l10n_util::GetStringUTF16(IDS_APP_CLOSE));
    default_dismiss_button.type = message_center::ButtonType::DISMISS;
    for (auto& button : buttons) {
      button.type = message_center::ButtonType::ACKNOWLEDGE;
    }
    // Insert the default dismiss button at the end of the vector and reset the
    // notification buttons.
    buttons.push_back(default_dismiss_button);
    notification.set_buttons(buttons);
  }

  return notification;
}

std::u16string PlatformNotificationServiceImpl::DisplayNameForContextMessage(
    const GURL& origin) const {
#if BUILDFLAG(ENABLE_EXTENSIONS_CORE)
  // If the source is an extension, lookup the display name.
  if (origin.SchemeIs(extensions::kExtensionScheme)) {
    const extensions::Extension* extension =
        extensions::ExtensionRegistry::Get(profile_)->GetExtensionById(
            origin.GetHost(), extensions::ExtensionRegistry::EVERYTHING);
    DCHECK(extension);

    return base::UTF8ToUTF16(extension->name());
  }
#endif

  return std::u16string();
}

std::optional<webapps::AppId> PlatformNotificationServiceImpl::FindWebAppId(
    const GURL& web_app_hint_url) const {
#if !BUILDFLAG(IS_ANDROID)
  web_app::WebAppProvider* web_app_provider =
      web_app::WebAppProvider::GetForLocalAppsUnchecked(profile_);
  if (web_app_provider) {
    return web_app_provider->registrar_unsafe().FindBestAppWithUrlInScope(
        web_app_hint_url, web_app::WebAppFilter::InstalledInChrome());
  }
#endif

  return std::nullopt;
}

std::optional<PlatformNotificationServiceImpl::WebAppIconAndTitle>
PlatformNotificationServiceImpl::FindWebAppIconAndTitle(
    const GURL& web_app_hint_url) const {
#if !BUILDFLAG(IS_ANDROID)
  web_app::WebAppProvider* web_app_provider =
      web_app::WebAppProvider::GetForLocalAppsUnchecked(profile_);
  if (web_app_provider) {
#if BUILDFLAG(IS_CHROMEOS)
    // The PlatformNotificationServiceTest FindWebAppIconAndTitle seems to be
    // verifying the availability of an icon and a title for notification
    // purposes, even though the app is not installed with OS integration, which
    // is surprising.
    web_app::WebAppFilter filter = web_app::WebAppFilter::InstalledInChrome();
#else
    web_app::WebAppFilter filter =
        web_app::WebAppFilter::SupportsOsNotifications();
#endif
    const std::optional<webapps::AppId> app_id =
        web_app_provider->registrar_unsafe().FindBestAppWithUrlInScope(
            web_app_hint_url, filter);
    if (app_id) {
      std::optional<WebAppIconAndTitle> icon_and_title;
      icon_and_title.emplace();

      icon_and_title->title = base::UTF8ToUTF16(
          web_app_provider->registrar_unsafe().GetAppShortName(*app_id));
      icon_and_title->icon =
          web_app_provider->icon_manager().GetMonochromeFavicon(*app_id);
      return icon_and_title;
    }
  }
#endif

  return std::nullopt;
}

bool PlatformNotificationServiceImpl::IsActivelyInstalledWebAppScope(
    const GURL& web_app_url) const {
#if BUILDFLAG(IS_ANDROID)
  // TODO(peter): Investigate whether it makes sense to consider installed
  // WebAPKs and TWAs on Android here, when depending features are considered.
  return false;
#else
  web_app::WebAppProvider* web_app_provider =
      web_app::WebAppProvider::GetForLocalAppsUnchecked(profile_);
  if (!web_app_provider) {
    return false;
  }
  const std::optional<webapps::AppId> app_id =
      web_app_provider->registrar_unsafe().FindBestAppWithUrlInScope(
          web_app_url, web_app::WebAppFilter::SupportsOsNotifications());
  return app_id.has_value();
#endif
}

void PlatformNotificationServiceImpl::
    HandleOnDeviceModelResponseThenMaybeDisplay(
        const message_center::Notification& notification,
        std::unique_ptr<PersistentNotificationMetadata> persistent_metadata,
        bool should_show_warning,
        std::optional<std::string> serialized_content_detection_metadata) {
  bool suspicious_notification_revoked = false;
  if (base::FeatureList::IsEnabled(
          safe_browsing::kAutoRevokeSuspiciousNotification) &&
      should_show_warning) {
#if BUILDFLAG(IS_ANDROID)
    suspicious_notification_revoked = AbusiveNotificationPermissionsManager::
        MaybeRevokeSuspiciousNotificationPermission(profile_,
                                                    notification.origin_url());
#endif

    auto* service =
        NotificationsEngagementServiceFactory::GetForProfile(profile_);
    // This service might be missing for incognito profiles and in tests.
    if (!suspicious_notification_revoked && service) {
      // Increment suspicious count if the notification permission has not been
      // revoked.
      service->RecordNotificationSuspicious(notification.origin_url());
    }
  }
  if (base::FeatureList::IsEnabled(
          safe_browsing::kReportNotificationContentDetectionData)) {
    // If the notification permission has been revoked, we do still want to
    // record the notification in the database for re-grant scenario; however;
    // there is no need to trigger `DidUpdatePersistentMetadata` callback.
    content::PlatformNotificationContext::WriteResourcesResultCallback
        callback = suspicious_notification_revoked
                       ? base::DoNothing()
                       : base::BindOnce(&PlatformNotificationServiceImpl::
                                            DidUpdatePersistentMetadata,
                                        weak_ptr_factory_.GetWeakPtr(),
                                        std::move(persistent_metadata),
                                        notification, should_show_warning);
#if BUILDFLAG(IS_ANDROID)
    if (should_show_warning && !suspicious_notification_revoked) {
      // Keep track of suspicious notification ids.
      safe_browsing::UpdateSuspiciousNotificationIds(
          HostContentSettingsMapFactory::GetForProfile(profile_),
          notification.origin_url(), notification.id());
    }
#endif
    if (serialized_content_detection_metadata.has_value()) {
      scoped_refptr<content::PlatformNotificationContext> notification_context =
          profile_->GetStoragePartitionForUrl(notification.origin_url())
              ->GetPlatformNotificationContext();
      if (notification_context) {
        notification_context->WriteNotificationMetadata(
            notification.id(), notification.origin_url(),
            safe_browsing::kNotificationContentDetectionMetadataDictionaryKey,
            serialized_content_detection_metadata.value(), std::move(callback));
        return;
      }
    }
    std::move(callback).Run(/*success=*/false);
  } else {
    // Notification permission has been revoked due to suspicious content; do
    // not show notification.
    if (suspicious_notification_revoked) {
      return;
    }
    DoUpdatePersistentMetadataThenDisplay(std::move(persistent_metadata),
                                          notification, should_show_warning);
  }
}

void PlatformNotificationServiceImpl::LogPersistentNotificationShownMetrics(
    const blink::PlatformNotificationData& notification_data,
    const GURL& origin,
    const GURL& notification_origin) {
  NotificationMetricsLoggerFactory::GetForBrowserContext(profile_)
      ->LogPersistentNotificationShown();

  auto* service =
      NotificationsEngagementServiceFactory::GetForProfile(profile_);
  // This service might be missing for incognito profiles and in tests.
  if (service) {
    service->RecordNotificationDisplayed(notification_origin);
  }

  permissions::PermissionUmaUtil::RecordPermissionUsage(
      ContentSettingsType::NOTIFICATIONS, profile_, nullptr,
      notification_origin);
}

bool PlatformNotificationServiceImpl::
    AreSuspiciousNotificationsAllowlistedByUser(const GURL& origin) {
  auto* hcsm = HostContentSettingsMapFactory::GetForProfile(profile_);
  if (!hcsm || !origin.is_valid()) {
    return false;
  }
  content_settings::SettingInfo info;
  base::Value stored_value(hcsm->GetWebsiteSetting(
      origin, origin,
      ContentSettingsType::ARE_SUSPICIOUS_NOTIFICATIONS_ALLOWLISTED_BY_USER,
      &info));
  if (stored_value.is_none()) {
    return false;
  }
  if (!stored_value.is_dict() || !stored_value.GetDict().contains(
                                     safe_browsing::kIsAllowlistedByUserKey)) {
    return false;
  }
  return stored_value.GetDict()
      .FindBool(safe_browsing::kIsAllowlistedByUserKey)
      .value_or(false);
}

void PlatformNotificationServiceImpl::DidUpdatePersistentMetadata(
    std::unique_ptr<PersistentNotificationMetadata> persistent_metadata,
    message_center::Notification notification,
    bool should_show_warning,
    bool success) {
  DoUpdatePersistentMetadataThenDisplay(std::move(persistent_metadata),
                                        notification, should_show_warning);
}

void PlatformNotificationServiceImpl::DoUpdatePersistentMetadataThenDisplay(
    std::unique_ptr<PersistentNotificationMetadata> persistent_metadata,
    message_center::Notification notification,
    bool should_show_warning) {
  base::UmaHistogramEnumeration(
      kNotificationContentDetectionDisplayPersistentNotificationEventHistogram,
      DisplayPersistentNotificationEvents::kFinished);
  persistent_metadata->is_suspicious = should_show_warning;
  NotificationDisplayServiceFactory::GetForProfile(profile_)->Display(
      NotificationHandler::Type::WEB_PERSISTENT, notification,
      std::move(persistent_metadata));
}
