chromium/chrome/browser/notifications/mac/notification_dispatcher_mojo.cc

// Copyright 2021 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/mac/notification_dispatcher_mojo.h"

#include <set>
#include <utility>
#include <vector>

#include "base/cancelable_callback.h"
#include "base/containers/flat_set.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/metrics/histogram_functions.h"
#include "base/sequence_checker.h"
#include "base/time/time.h"
#include "chrome/browser/notifications/mac/mac_notification_provider_factory.h"
#include "chrome/browser/notifications/mac/notification_utils.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"

namespace {

// The initial delay for restarting the notification service. An exponential
// backoff will double this value whenever the OneShotTimer reschedules.
constexpr base::TimeDelta kInitialServiceRestartTimerDelay =
    base::Milliseconds(500);
// Maximum delay between restart attempts. We don't want this to be too low to
// avoid heavy resource usage but also not too high keep notifications working.
constexpr base::TimeDelta kMaximumServiceRestartTimerDelay = base::Seconds(256);
// If the service ran for more than this time we will reset the restart delay to
// |kInitialServiceRestartTimerDelay|.
constexpr base::TimeDelta kServiceRestartTimerResetDelay = base::Seconds(10);

}  // namespace

NotificationDispatcherMojo::NotificationDispatcherMojo(
    std::unique_ptr<MacNotificationProviderFactory> provider_factory)
    : provider_factory_(std::move(provider_factory)),
      next_service_restart_timer_delay_(kInitialServiceRestartTimerDelay) {
  // Force start the notification service once so we show the permission request
  // to users on the first start of Chrome.
  // TODO(crbug.com/40149365): Find a better time to ask for permissions.
  CheckIfServiceCanBeTerminated();
}

NotificationDispatcherMojo::~NotificationDispatcherMojo() = default;

void NotificationDispatcherMojo::DisplayNotification(
    NotificationHandler::Type notification_type,
    Profile* profile,
    const message_center::Notification& notification) {
  no_notifications_checker_.Cancel();
  GetOrCreateService()->DisplayNotification(
      CreateMacNotification(notification_type, profile, notification));
}

void NotificationDispatcherMojo::CloseNotificationWithId(
    const MacNotificationIdentifier& identifier) {
  if (HasNoDisplayedNotifications())
    return;

  GetOrCreateService()->CloseNotification(
      mac_notifications::mojom::NotificationIdentifier::New(
          identifier.notification_id,
          mac_notifications::mojom::ProfileIdentifier::New(
              identifier.profile_id, identifier.incognito)));
  CheckIfServiceCanBeTerminated();
}

void NotificationDispatcherMojo::CloseNotificationsWithProfileId(
    const std::string& profile_id,
    bool incognito) {
  if (HasNoDisplayedNotifications())
    return;

  GetOrCreateService()->CloseNotificationsForProfile(
      mac_notifications::mojom::ProfileIdentifier::New(profile_id, incognito));
  CheckIfServiceCanBeTerminated();
}

void NotificationDispatcherMojo::CloseAllNotifications() {
  if (HasNoDisplayedNotifications())
    return;

  if (service_)
    service_->CloseAllNotifications();
  OnServiceDisconnectedGracefully(ShutdownType::kChromeInitiated);
}

void NotificationDispatcherMojo::GetDisplayedNotificationsForProfileId(
    const std::string& profile_id,
    bool incognito,
    GetDisplayedNotificationsCallback callback) {
  if (HasNoDisplayedNotifications()) {
    std::move(callback).Run(/*notification_ids=*/{},
                            /*supports_synchronization=*/true);
    return;
  }

  no_notifications_checker_.Cancel();
  GetOrCreateService()->GetDisplayedNotifications(
      mac_notifications::mojom::ProfileIdentifier::New(profile_id, incognito),
      /*origin=*/std::nullopt,
      base::BindOnce(&NotificationDispatcherMojo::DispatchGetNotificationsReply,
                     base::Unretained(this), std::move(callback)));
}

void NotificationDispatcherMojo::GetDisplayedNotificationsForProfileIdAndOrigin(
    const std::string& profile_id,
    bool incognito,
    const GURL& origin,
    GetDisplayedNotificationsCallback callback) {
  if (HasNoDisplayedNotifications()) {
    std::move(callback).Run(/*notification_ids=*/{},
                            /*supports_synchronization=*/true);
    return;
  }

  no_notifications_checker_.Cancel();
  GetOrCreateService()->GetDisplayedNotifications(
      mac_notifications::mojom::ProfileIdentifier::New(profile_id, incognito),
      origin,
      base::BindOnce(&NotificationDispatcherMojo::DispatchGetNotificationsReply,
                     base::Unretained(this), std::move(callback)));
}

void NotificationDispatcherMojo::GetAllDisplayedNotifications(
    GetAllDisplayedNotificationsCallback callback) {
  if (HasNoDisplayedNotifications()) {
    std::move(callback).Run(/*notification_ids=*/{});
    return;
  }

  no_notifications_checker_.Cancel();
  GetOrCreateService()->GetDisplayedNotifications(
      /*profile=*/nullptr, /*origin=*/std::nullopt,
      base::BindOnce(
          &NotificationDispatcherMojo::DispatchGetAllNotificationsReply,
          base::Unretained(this), std::move(callback)));
}

void NotificationDispatcherMojo::OnNotificationAction(
    mac_notifications::mojom::NotificationActionInfoPtr info) {
  ProcessMacNotificationResponse(provider_factory_->notification_style(),
                                 std::move(info));
  CheckIfServiceCanBeTerminated();
}

void NotificationDispatcherMojo::UserInitiatedShutdown() {
  OnServiceDisconnectedGracefully(ShutdownType::kUserInitiated);
}

void NotificationDispatcherMojo::CheckIfServiceCanBeTerminated() {
  no_notifications_checker_.Reset(base::BindOnce(
      &NotificationDispatcherMojo::OnServiceDisconnectedGracefully,
      base::Unretained(this), ShutdownType::kChromeInitiated));

  // The service will indicate it is okay to be terminated if there are no
  // displayed notifications left, and (in the case of the UNNotification API)
  // there are no pending permission requests either.
  // If this happens, we close the mojo connection (only if the callback has not
  // been canceled yet).
  GetOrCreateService()->OkayToTerminateService(base::BindOnce(
      [](base::OnceClosure disconnect_closure, bool can_terminate) {
        if (can_terminate) {
          std::move(disconnect_closure).Run();
        }
      },
      no_notifications_checker_.callback()));
}

void NotificationDispatcherMojo::OnServiceDisconnectedGracefully(
    ShutdownType shutdown_type) {
  base::TimeDelta elapsed = base::TimeTicks::Now() - service_start_time_;

  // Log utility process runtime metrics to UMA.
  if (service_) {
    auto emit_time_histogram = [elapsed](const char* name) {
      base::UmaHistogramCustomTimes(name, elapsed, base::Milliseconds(100),
                                    base::Hours(8),
                                    /*buckets=*/50);
    };
    switch (provider_factory_->notification_style()) {
      case mac_notifications::NotificationStyle::kBanner:
        // No need to collect metrics for in-process notifications.
        break;
      case mac_notifications::NotificationStyle::kAlert:
        emit_time_histogram("Notifications.macOS.ServiceProcessRuntime");
        if (shutdown_type == ShutdownType::kUnexpected) {
          emit_time_histogram("Notifications.macOS.ServiceProcessKilled");
        }
        break;
      case mac_notifications::NotificationStyle::kAppShim:
        emit_time_histogram(
            "Notifications.macOS.AppShimProcess.UptimeOnDisconnect");
        if (shutdown_type == ShutdownType::kUnexpected) {
          emit_time_histogram(
              "Notifications.macOS.AppShimProcess."
              "UptimeOnUnexpectedDisconnect");
        }
        break;
    }
  }

  // If the service ran for more than 10 seconds or completed successfully we
  // restore the next delay to the initial value. If it failed sooner than that
  // then double the next time until we hit a maximum value so we don't end up
  // restarting it a lot.
  if (elapsed > kServiceRestartTimerResetDelay ||
      shutdown_type == ShutdownType::kChromeInitiated) {
    next_service_restart_timer_delay_ = kInitialServiceRestartTimerDelay;
  } else if (next_service_restart_timer_delay_ <
             kMaximumServiceRestartTimerDelay) {
    next_service_restart_timer_delay_ = next_service_restart_timer_delay_ * 2;
  }

  if (shutdown_type == ShutdownType::kUnexpected) {
    // Calling CheckIfServiceCanBeTerminated() will force a new connection
    // attempt. base::Unretained(this) is safe here because |this| owns
    // |service_restart_timer_|.
    service_restart_timer_.Start(
        FROM_HERE, next_service_restart_timer_delay_,
        base::BindOnce(
            &NotificationDispatcherMojo::CheckIfServiceCanBeTerminated,
            base::Unretained(this)));
  } else {
    service_restart_timer_.AbandonAndStop();
  }

  no_notifications_checker_.Cancel();
  provider_.reset();
  service_.reset();
  handler_.reset();
}

bool NotificationDispatcherMojo::HasNoDisplayedNotifications() const {
  return !service_ && !service_restart_timer_.IsRunning();
}

mac_notifications::mojom::MacNotificationService*
NotificationDispatcherMojo::GetOrCreateService() {
  if (!service_) {
    service_restart_timer_.AbandonAndStop();
    service_start_time_ = base::TimeTicks::Now();
    provider_ = provider_factory_->LaunchProvider();
    provider_.set_disconnect_handler(base::BindOnce(
        &NotificationDispatcherMojo::OnServiceDisconnectedGracefully,
        base::Unretained(this), ShutdownType::kUnexpected));
    provider_->BindNotificationService(service_.BindNewPipeAndPassReceiver(),
                                       handler_.BindNewPipeAndPassRemote());
  }
  return service_.get();
}

void NotificationDispatcherMojo::DispatchGetNotificationsReply(
    GetDisplayedNotificationsCallback callback,
    std::vector<mac_notifications::mojom::NotificationIdentifierPtr>
        notifications) {
  std::set<std::string> notification_ids;

  for (const auto& notification : notifications)
    notification_ids.insert(notification->id);

  // Check if there are any notifications left after this.
  if (notification_ids.empty())
    CheckIfServiceCanBeTerminated();

  std::move(callback).Run(std::move(notification_ids),
                          /*supports_synchronization=*/true);
}

void NotificationDispatcherMojo::DispatchGetAllNotificationsReply(
    GetAllDisplayedNotificationsCallback callback,
    std::vector<mac_notifications::mojom::NotificationIdentifierPtr>
        notifications) {
  std::vector<MacNotificationIdentifier> notification_ids;

  for (const auto& notification : notifications) {
    notification_ids.push_back({notification->id, notification->profile->id,
                                notification->profile->incognito});
  }

  // Check if there are any notifications left after this.
  if (notification_ids.empty())
    CheckIfServiceCanBeTerminated();

  // Initialize the base::flat_set via a std::vector to avoid N^2 runtime.
  base::flat_set<MacNotificationIdentifier> identifiers(
      std::move(notification_ids));
  std::move(callback).Run(std::move(identifiers));
}