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